31 Animation

gksrudtlr
|2025. 1. 12. 18:27

Animation

UV 맵핑

  • 애니메이션은 여러개의 사진이 있는 스프라이트에서 일정 영역별로 잘라서 순서대로 이미지를 바꿔보여주는 것이다
  • 이때 일일히 잘라서 사용하는 것이 아닌 UV 맵핑을 통해 특정 범위의 사각형을 그려 우리가 원하는 이미지를 그 영역에 맞춰 잘라 그리는 방법을 사용할 것이다

    Animator

  • Animator는 애니메이션을 실행시켜주는 플레이어라 생각하면 된다
  • Animation 리소스들을 가져와 순서대로 실행시켜 특정 동작을 하게 만드는 것이다
  • 물체마다 존재하는 컴포넌트로 리소스는 따로 존재하지만 리소스를 가져와 재생해준다

    Animation Class

    struct Keyframe
    {
      Vec2 offset = Vec2{ 0.0f,0.0f };
      Vec2 size = Vec2{ 0.0f,0.0f };
      float time = 0.0f ;
    };
    

class Animation : public ResourceBase
{
using Super = ResourceBase;
public:
Animation();
virtual ~Animation();

virtual void Load(const wstring& path) override;
virtual void Save(const wstring& path) override;

void SetLoop(bool loop);
bool IsLoop() { return bLoop; }

void SetTexture(shared_ptr<Texture> texture);
shared_ptr<Texture> GetTexture() { return m_pTexture; }

Vec2 GetTextureSize();

const Keyframe& GetKeyframe(int32 index);
int32 GetKeyframeCount();
void AddKeyframe(const Keyframe& keyframe);

private:
bool bLoop = false;
shared_ptr m_pTexture;
vector m_vKeyframes;
};

cpp
Animation::Animation() : Super(ResourceType::Animation)
{
}

Animation::~Animation()
{
}

void Animation::Load(const wstring& path)
{
ResourceBase::Load(path);
}

void Animation::Save(const wstring& path)
{
ResourceBase::Save(path);
}

void Animation::SetLoop(bool loop)
{
bLoop = loop;
}

void Animation::SetTexture(shared_ptr texture)
{
m_pTexture = texture;
}

Vec2 Animation::GetTextureSize()
{
return m_pTexture->GetSize();
}

const Keyframe& Animation::GetKeyframe(int32 index)
{
return m_vKeyframes[index];
}

int32 Animation::GetKeyframeCount()
{
return static_cast(m_vKeyframes.size());
}

void Animation::AddKeyframe(const Keyframe& keyframe)
{
m_vKeyframes.push_back(keyframe);
}

- Animation Class에 struct로 offset과 size를 vector2를 이용해 크기를 지정해준 뒤 재생될 시간인 float time 변수도 선언해준다
- Animation이 실행되기 위해서는 Texture가 있어야 하므로 객체를 만들어주고, vector 배열을 통해 keyframe들을 저장할 수 있는 공간도 만들어준다
- 이때 Animation이 반복할수도 있으므로 bool형 변수 bLoop도 만들어줬다
- Load와 Save 함수를 재정의하여 파일로 사용할 수 있게 만들것이다
- 또한 Loop를 할지 안할지 지정해주는 Set 함수와 외부에서 Loop인지 아닌지를 가져올 수 있는 Get 함수를 만들어준다
- 마찬가지로 Texture를 외부에서 정해주는 Set함수와 외부에서 Texture를 사용할 수 있는 Get 함수도 만들어준 뒤 Texture의 크기를 리턴해주는 함수를 만들어 줄 것이다
- 마지막으로 Keyframe을 index로 가져오는 함수와 Keyframe의 갯수를 리턴받는 함수, Keyframe을 추가해주는 함수까지 만들어준다
### TextureSize() 추가
```C++
class Texture
{
public:
    Vec2 GetSize(){return size;}
private:
    Vec2 size;
}
cpp
...
void Texture::Create(const wstring& path)
{
...
    size.x = md.width;
    size.y = md.height;
}
  • Texture의 크기를 리턴받는 함수를 Texture에서 구현해주고, size는 각각 DirectX::TexMetadata 변수에 저장된 width, height를 x,y 좌표에 저장해주면 된다

    Animator Class

    class Animator : public Component
    {
      using Super = Component;
    public:
      Animator();
      virtual ~Animator();
    
      void Init();
      void Update();
    
      shared_ptr<Animation> GetCurrentAnimation();
      const Keyframe& GetCurrentKeyframe();
      void SetAnimation(shared_ptr<Animation> animation);
    

private:
float m_fSumTime = 0.0f;
int32 m_nCurrentKeyframeIndex = 0;
shared_ptr m_pCurrentAnimation;
};

cpp
...
void Animator::Update()
{
Component::Update();
shared_ptr animation = GetCurrentAnimation();
if(animation == nullptr)
return;

const Keyframe& keyframe = animation->GetKeyframe(m_nCurrentKeyframeIndex);
float deltaTime = TIME->GetDeltaTime();
m_fSumTime += deltaTime;

if(m_fSumTime >= keyframe.time)
{
    m_nCurrentKeyframeIndex++;
    int32 totalCount = animation->GetKeyframeCount();

    if(m_nCurrentKeyframeIndex >= totalCount)
    {
        if (animation->IsLoop() == true)
            m_nCurrentKeyframeIndex = 0;
        else
            m_nCurrentKeyframeIndex = totalCount - 1;
    }

    m_fSumTime = 0.0f;
}

}

shared_ptr Animator::GetCurrentAnimation()
{
return m_pCurrentAnimation;
}

const Keyframe& Animator::GetCurrentKeyframe()
{
return m_pCurrentAnimation->GetKeyframe(m_nCurrentKeyframeIndex);
}

void Animator::SetAnimation(shared_ptr animation)
{
m_pCurrentAnimation = animation;
}

- Animator는 위에서 얘기했듯 Animation을 재생해주는 재생기 역할을 하는 것이고, Animation 클래스는 재생될 CD라 생각하면 된다
- 그래서 시간을 계속 누적해줄 float 변수 m_fSumTime, 현재 keyframe index를 저장할 변수, 현재 Animation이 뭔지 저장할 변수들을 만들어준다
- 그리고 Component class에서 Init함수와 Update를 재정의하고, 현재 Animation을 return해주는 함수, 현재 Keyframe을 반환해주는 함수, Animation을 지정해주는 함수들을 정의해준다
- Update 함수에서는 Animation의 keyframe을 바탕으로 현재 Animation을 실행할지 바꿔줄지 판단해 Update를 해줄 것이다
- 그렇기 때문에 객체를 생성해 GetCurrentAnimation 함수의 리턴된 값을 저장해주고, 그 값이 null이라면 현재 재생중인 Animation이 없는 것이므로 그대로 함수를 종료해준다
- 그렇지 않다면 keyframe을 가져와 임시로 저장해주고, TimeMGR Class에서 deltaTime을 가져와 m_fSumTime에 누적해서 저장해준다
- 이때 m_fSumTime이 keyframe.time보다 크거나 같을 경우 다음 Animation이 재생되어야한다
- 그렇기 때문에 현재 keyframe index를 하나 늘려주고, Animation에 GetKeyframeCount를 가져와 임시로 저장해준다
- 그 뒤 두 값을 비교할 것인데, 그 이유는 현재 keyframe index가 Animation에 저장된 keyframe 배열의 크기보다 크거나 같으면 마지막 Animation까지 모두 재생된 것 이므로 다른 조취를 취해야하기 때문이다
- 그래서 현재 keyframe index가 더 크거나 같다면 Animation에서 Loop처리를 해줬는지를 판단해 Loop가 true면 현재 keyframe index를 0으로 만들어 처음부터 다시 실행되게 해주고, 그렇지 않다면 Animation의 Keyframe크기에 -1을 해준 값을 저장하여 마지막 Animation을 계속해서 재생할 수 있게 해준다
- 그 후 m_fSumTime을 0으로 만들어주어 현재 Animation을 다시 실행할 수 있도록 해준다
### ResourceMGR 추가
```C++
ResourceMGR.cpp
...
void ResourceMGR::CreateDefaultTexture()
{
...
    {
        shared_ptr<Texture> texture = make_shared<Texture>(m_pDevice);
        texture->SetName(L"Snake");
        texture->Create(L"Snake.bmp");
        Add(texture->GetName(), texture);
    }
}
...
void ResourceMGR::CreateDefaultAnimation()
{
    shared_ptr<Animation> animation = make_shared<Animation>();
    animation->SetName(L"SnakeAnim");
    animation->SetTexture(Get<Texture>(L"Snake"));
    animation->SetLoop(true);

    animation->AddKeyframe(Keyframe{ Vec2{0.0f,0.0f}, Vec2{100.0f,100.0f}, 0.1f });
    animation->AddKeyframe(Keyframe{ Vec2{100.0f,0.0f}, Vec2{100.0f,100.0f}, 0.1f });
    animation->AddKeyframe(Keyframe{ Vec2{200.0f,0.0f}, Vec2{100.0f,100.0f}, 0.1f });
    animation->AddKeyframe(Keyframe{ Vec2{300.0f,0.0f}, Vec2{100.0f,100.0f}, 0.1f });

    Add(animation->GetName(), animation);
}
  • Animation을 띄우기 위해서 ResourceMGR에서 Texture와 Animation 리소스를 만들어 추가해줘야 한다

  • 우리가 계속 띄었던 Texture 밑에 새로운 Texture를 생성해 줄것이다

  • 또한 Animation을 추가하기 위해 이름을 지어주고, Texture로부터 이름으로 가져와 Texture를 셋팅해준다

  • Loop는 해도되고 안해도 되지만 계속해서 움직이는 Animation이므로 Loop를 true로 해줬다

  • Animation은 총 4개이기 때문에 AddKeyframe을 4번 호출해줬다

  • 이 이미지 파일은 offset이 한칸당 100, 크기도 100 * 100인 이미지여서 offset을 0부터 100씩 늘려주고, size는 모두 100,100으로 넣었다

  • offset에 y가 0인 이유는 가로로만 사용하기 때문이고, 재생 속도는 0.1로 넘겨주었다

  • 이렇게 설정 후 Add 함수를 통해 Animation 리소스를 추가해주면 된다

    SceneMGR 추가

  • ResourceMGR에 리소스를 추가해줬으니 이제 Scene에 띄우기 위해 Scene에 생성을 해줘야 한다

    SceneManager.cpp
    ...
    shared_ptr<Scene> SceneManager::LoadTestScene()
    {
    ...
      {
          shared_ptr<Animator> animator = make_shared<Animator>();
          m_pMonster->AddComponent(animator);
          shared_ptr<Animation> anim = RESOURCES->Get<Animation>(L"SnakeAnim");
          animator->SetAnimation(anim);
      }
    ...
    }
    ...
  • m_pMonster 오브젝트를 만들었던 곳 밑에 Animator를 생성해 Component를 추가해준뒤 리소스로 만든 Animation을 key값으로 찾아와 Animator에 셋팅해주면 된다

    영역 만들기

  • 리소스를 추가하고 Component도 추가하고 오브젝트에 넣어줬지만 정작 우리가 Animation을 그릴 이미지의 영역을 나눠 Shader로 넘겨주는 부분을 설정하진 않았다

    RenderHelper.h
    ...
    struct AnimationData
    {
      Vec2 textureSize;
      Vec2 spriteOffset;
      Vec2 spriteSize;
      float useAnimation;
      float padding;
    };
  • 이를 위해 RenderHelper.h에 전체 texture의 크기, Shader로 잘라 넘겨줄 offset, size를 설정하는 구조체를 만들어준다

  • Shader에서는 float4를 사용하므로 16바이트 단위로 맞춰 넘겨줘야 하는데 지금까지 위의 구조체들은 Matrix를 사용해 16바이트 단위로 정렬을 맞춰 넘겨줬는데 지금은 vector2D를 사용하므로 Shader에 넘겨줄 때 16바이트 단위로 맞춰서 넘겨줄 필요가 있다

  • 현재 vector2D가 3개로 24바이트가 되면서 8바이트가 부족하다

  • 우리가 현재 Animation을 사용할 때 vector2D 값들과 함께 Animation을 사용할지 안할지를 판단하는 값까지(0,1만 들어갈 값임) float으로 만들어 넘겨주면 28바이트로, 나머지 4바이트는 float형 쓰레기 값을 넘겨줄 것이다

    4바이트를 어거지로 채우기 위해서 float형 변수를 하나 더 사용했다는데 이는 더 공부하고 찾아봐서 더 좋은 방법이나 이 값을 사용할 방법이 있나 연구해봐야 할것같다)
    ### RenderMGR 추가
  • Render를 관리하는 RenderMGR에 AnimationData를 추가하여 GPU로 넘겨줄 것이다

    class RenderMGR
    {
    ...
    private:
    ...
      void PushAnimationData();
    ...
    public:
    ...
      AnimationData m_cAnimationData;
      shared_ptr<ConstantBuffer<AnimationData>> m_pAnimationBuffer;
    ...
    }
    cpp
    ...
    void RenderMGR::Init()
    {
    ...
      //CreateAnimationBuffer
      m_pAnimationBuffer = make_shared<ConstantBuffer<AnimationData>>(m_pDevice, m_pDeviceContext);
      m_pAnimationBuffer->Create();
    ...
    }
    ...
    void RenderMGR::PushAnimationData()
    {
      m_pAnimationBuffer->CopyData(m_cAnimationData);
    }
    ...
  • AnimationBuffer를 Init 함수에서 생성해주고, CPU에서 GPU로 값을 복사할 수 있게 Push 함수도 만들어 AnimationData 값을 ConstantBuffer로 복사해준다

  • GameObject는 Renderer가 있는지 확인했지만 MeshRenderer를 이용함과 동시에 Animation을 사용하면서 Animation을 재생해줘야 하는지 여부도 확인해야될 것이다

    void RenderMGR::RenderObjects()
    {
    ...
          //Animation
          shared_ptr<Animator> animator = gameObject->GetAnimator();
          if(animator != nullptr)
          {
              const Keyframe& keyframe = animator->GetCurrentKeyframe();
              m_cAnimationData.spriteOffset = keyframe.offset;
              m_cAnimationData.spriteSize = keyframe.size;
              m_cAnimationData.textureSize = animator->GetCurrentAnimation()->GetTextureSize();
              m_cAnimationData.useAnimation = 1.0f;
              PushAnimationData();
          }
    ...
    }
  • RenderObjects 함수에서 GameObject에서 Animator를 가져와 값이 null인지 판단한 후 null이 아닐때 animator의 현재 keyframe을 가져와 AnimationData의 값에 설정해준다

  • 설정이 끝나면 AnimationData를 ConstantBuffer에 복사해주기 위해 좀전에 만든 PushAnimationData 함수를 호출해준다

  • 이렇게 되면 ConstantBuffer에 저장된 정보를 가지고 Shader로 가지고 가 Animation을 그려줄 수 있을 것이다

    Sahder

  • 현재 Shader에는 AnimationData의 정보를 받아와 사용할 수 있는 부분이 없기 때문에 이를 추가해줘야한다

    ...
    cbuffer AnimationData : register(b2)
    {
      float2 spriteOffset;
      float2 spriteSize;
      float2 TextureSize;
      float useAnimation;
    }
    ...
  • 복사된 ConstantBuffer를 전달받기 위해 register에 새로 슬롯을 바인딩 해준다

  • CPU에서 GPU로 복사할땐 16바이트 단위로 맞춰 생성해서 복사해 줬지만 여기서는 16바이트 단위로 맞춰주지 않아도 된다

    이유 그 이유는 상수 버퍼의 메모리 레이아웃은 GPU 메모리와 Shader 사이에 효율적인 데이터 접근을 보장하기 위해 16바이트 단위로 정렬되기 때문이다
    ### RenderMGR ```C++ void RenderMGR::RenderObjects() { ... //Animation shared_ptr animator = gameObject->GetAnimator(); if(animator != nullptr) { const Keyframe& keyframe = animator->GetCurrentKeyframe(); m_cAnimationData.spriteOffset = keyframe.offset; m_cAnimationData.spriteSize = keyframe.size; m_cAnimationData.textureSize = animator->GetCurrentAnimation()->GetTextureSize(); m_cAnimationData.useAnimation = 1.0f; PushAnimationData();
          m_pPipeline->ConstantBuffer(2, EVertexShader, m_pAnimationBuffer);
          m_pPipeline->SetTexture(0, EPixelShader, animator->GetCurrentAnimation()->GetTexture());
      }
      else
      {
          m_cAnimationData.spriteOffset = Vec2(0.0f,0.0f);
          m_cAnimationData.spriteSize = Vec2(0.0f, 0.0f);
          m_cAnimationData.textureSize = Vec2(0.0f, 0.0f);
          m_cAnimationData.useAnimation = 0.0f;
          PushAnimationData();
    
          m_pPipeline->ConstantBuffer(2, EVertexShader, m_pAnimationBuffer);
          m_pPipeline->SetTexture(0, EPixelShader, meshRenderer->GetTexture());
      }

...
//m_pPipeline->SetTexture(0, EPixelShader, meshRenderer->GetTexture());
...
}
}

- Shader에 AnimationBuffer를 복사한 값을 받을 수 있게 슬롯을 바인딩했으므로 넘겨줄 것이다
- m_pPipeline의 ConstantBuffer 함수를 통해 지정된 슬롯의 번호와 Scope, 버퍼를 넘겨준뒤 Texture도 현재 Animation Texture로 넘겨준다
- 이때 Scope는 예전에 ShaderBase.h에서 enum으로 만들었던 것중 VertexShader인지 PixelShader인지 둘 다인지 판단하기 위해 만든 것으로 ConstantBuffer 함수에서 이 Scop로 어떤 것인지 판단하여 deviceCotext에서 알맞은 함수를 콜하여 설정해주는 역할을 한다
- 만약 Animator가 null이라면 기존의 Texture를 띄우게 할것이므로 AnimationData의 값은 모두 0으로 설정해서 ConstantBuffer에 복사해주고 GUP로 넘겨준다
- 이때 Texture 설정을 meshRender에 있는 Texture로 넘겨주면 계속 띄었던 그 이미지가 뜰것이다
- 이렇게 Animation이 없을 때 Texture도 설정해줬으므로 밑에서 Texture를 설정했던 부분은 지워준다
### Shader에 적용
- ![](https://velog.velcdn.com/images/gksrudtlr2/post/82ce02ca-1b82-42b8-9c1e-2b52b70ff973/image.png)현재 실행을 하게되면 Shader에 AnimationBuffer 셋팅은 됐지만 이를 이용해 영역을 나눠 자르고 시간이 지나면 다른 이미지로 그려주는 코드는 어디에도 없기 때문에 위의 사진처럼 애니메이션으로 사용하려는 png 파일 전체가 그려질 것이다
```HLSL
VS_OUTPUT VS(VS_INPUT input)
{
...
    if(useAnimation == 1.0f)
    {
        output.uv *= spriteSize / TextureSize;
        output.uv += spriteOffset / TextureSize;
    }   
...
}
  • VertexShader 부분에 uv를 설정한 뒤 useAnimation이 1이면 uv에 spriteSize/TextureSize를 곱해주고, 또 uv에 spriteOffset / TextureSize를 더해줘 전체 크기에서 원하는 sprite의 크기와 위치를 UV 맵핑을 통해 그려줄 수 있게 된다

    문제 발생

  1. uv에 float2 변수 저장안됨 현상
    • Shader 부분에서 uv값에 float2 값을 저장해줄 때 문제가 생겨 봤더니 uv가 float4로 되어있었다
    • uv는 x,y축 두개만 이용하는 좌표계이므로 float2를 사용해줘야하는데 color를 지우고 uv로 바꿀 때 오타가 난것같다
  2. 사각형만 그려지고 이미지 안뜸
    • 실행시 이런식으로 이미지가 그려졌다
    • Animation buffer가 문제인지 값이 잘못 들어갔는지 확인을 해봤지만 도저히 못찾던 도중 RenderHelper에 AnimationData에 변수들의 순서와 Shader에서 AnimationBuffer를 가져오는 함수에 변수들의 순서가 다른것을 확인했다
    • 입력한 값을 저장한 순서가 달라 VS 단계에서 uv맵핑시 잘못된 값으로 매핑됐기 때문에 생기는 문제였다
    • 해결하는 방법은 변수들의 이름 순서를 동일하게 맞춰주던지 맵핑시 올바른 변수로 맵핑되게 연산을 바꾸던지 하는 방법이 있는데 이름 순서를 맞춰주는 방법으로 고쳤다

      Monster 추가

  • 이제 애니메이션도 그렸고 Monster object도 쉽게 추가할 수 있다

    SceneManager.cpp
    ...
    shared_ptr<Scene> SceneManager::LoadTestScene()
    {
      //monster
      {
          shared_ptr<GameObject> m_pMonster = make_shared<GameObject>(m_pGraphics->GetDevice(), m_pGraphics->GetDeviceContext());
          m_pMonster->GetOrAddTransform()->SetPosition(Vec3{ 1.0f,1.0f,0.0f });
          {
              m_pMonster->GetOrAddTransform();
              shared_ptr<MeshRenderer> meshRenderer = make_shared<MeshRenderer>(m_pGraphics->GetDevice(), m_pGraphics->GetDeviceContext());
              m_pMonster->AddComponent(meshRenderer);
    
              shared_ptr<Material> material = RESOURCES->Get<Material>(L"Default");
              meshRenderer->SetMaterial(material);
    
              shared_ptr<Mesh> mesh = RESOURCES->Get<Mesh>(L"Rectangle");
              meshRenderer->SetMesh(mesh);
          }
          {
              shared_ptr<Animator> animator = make_shared<Animator>();
              m_pMonster->AddComponent(animator);
              shared_ptr<Animation> anim = GGame->GetResourceMGR()->Get<Animation>(L"SnakeAnim");
              animator->SetAnimation(anim);
          }
          scene->AddGameObject(m_pMonster);
      }
    }
  • SceneMGR에 Scene을 Load하는 함수에서 monster를 추가했던 것을 그대로 복붙하여 위치만 살짝 변경해주면 두개가 생성되는 것을 볼 수 있다

  • 이처럼 쉽게 object들을 추가할 수 있다

  • 이때 카메라 비율에 따라 보여지는 위치가 다르므로 그에 맞게 위치나 크기를 지정해주면 된다

'DirectX' 카테고리의 다른 글

18 Graphics  (0) 2025.01.12
17 SimpleMath 예제  (0) 2025.01.12
15 Projection 변환 행렬  (0) 2025.01.12
14 View 변환  (0) 2025.01.12
13 World 변환 행렬  (1) 2025.01.12