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
vector
};
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
{
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
}
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
};
cpp
...
void Animator::Update()
{
Component::Update();
shared_ptr
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
{
return m_pCurrentAnimation;
}
const Keyframe& Animator::GetCurrentKeyframe()
{
return m_pCurrentAnimation->GetKeyframe(m_nCurrentKeyframeIndex);
}
void Animator::SetAnimation(shared_ptr
{
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형 변수를 하나 더 사용했다는데 이는 더 공부하고 찾아봐서 더 좋은 방법이나 이 값을 사용할 방법이 있나 연구해봐야 할것같다)
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바이트 단위로 정렬되기 때문이다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에 적용
- 현재 실행을 하게되면 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 맵핑을 통해 그려줄 수 있게 된다
문제 발생
- uv에 float2 변수 저장안됨 현상
- Shader 부분에서 uv값에 float2 값을 저장해줄 때 문제가 생겨 봤더니 uv가 float4로 되어있었다
- uv는 x,y축 두개만 이용하는 좌표계이므로 float2를 사용해줘야하는데 color를 지우고 uv로 바꿀 때 오타가 난것같다
- 사각형만 그려지고 이미지 안뜸
- 실행시 이런식으로 이미지가 그려졌다
- 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 |