1. 17. TweenDemo.fx와 TweenDemo클래스를 생성 후 세팅하기
애니메이션에서는 twining이라는게 있다.
두 가지의 상태가 애니메이션이 바뀔 때 중간에 transition을 틀어주는 거를 얘기한다.
그걸 만들어 볼 건데 방금 만든 blending이랑 크게 다르지 않다.
다만 넘겨주는게 조금 지저분해진다.
tweening이랑 관련된 부분들이 조금 더 추가가 되면 된다.
키 프레임을 더 확장시키건 KeyframeDesc를 포함하는 별도의 타입을 만들어서 하건 일단은 만들 수 있다.
쉐이더를 구분하는게 맞을 거 같으니 16. AnimationDemo.fx를 탐색기에서 복붙 해서 이름을 17. TweenDemo.fx로 해서 Client/Shader/Week3필터에 넣어준다.
AssimpTool/Game필터의 AnimationDemo클래스도 탐색기에서 복분해서 TweenDemo라고 한다. 코드를 TweenDemo에 맞게 수정한다.
TweenDemo::Init()에서
_shader = make_shared<Shader>(L"17. TweenDemo.fx");
이렇게 바꿔준다.
되던게 되고 안 되는 경우가 있어서 어지간해서는 분리를 해서 진행을 하고 있다.
AssimpTool/Main/Main.cpp에 가서
#include "TweenDemo.h"
desc.app = make_shared<TweenDemo>();
이렇게 바꿔준다.
실행해 보면 코드 내용은 그대로이기 때문에 잘 뜨는 걸 알 수 있다.
2. 17. TweenDemo.fx 쉐이더에서 struct TweenFrameDesc와 cbuffer TweenBuffer를 정의해 두 개의 애니메이션을 보간할 준비하기
쉐이더 먼저 작업하는게 편한 경우도 있고, 코드부터 만들고 쉐이더 만드는 게 편한 경우도 있다.
여기선 쉐이더부터 해본다.
17. TweenDemo.fx에서
struct KeyframeDesc
{
int animIndex;
uint currFrame;
uint nextFrame;
float ratio;
float sumTime;
float speed;
float2 padding;
};
이게 어떻게 보면 애니메이션 상태를 나타내는 것이고,
여기다가 두개의 애니메이션을 섞는 것을 고려한다면 struct TweenFrameDesc라는 걸 만들어 준다.
만약에 animIndex가 0보다 크거나 같아서 애니메이션이 예약이 되어 있다고 하면
struct TweenFrameDesc
{
float tweenDuration; // 얼마만큼 할지
float tweenRatio;
float tweenSumTime; // 경과시간
float padding;
KeyframeDesc curr; // 현재 애니메이션
KeyframeDesc next; // 바뀌어질 애니메이션(없으면 현재걸 트러주면 된다.)
};
여기 있는 정보를 이용해서 적절히 섞어서 만들어주면 된다.
cbuffer KeyframeBuffer
{
KeyframeDesc Keyframes;
};
는 이제 두 개의 struct가 통합된 것을 반영해야 하기 때문에 삭제하고
cbuffer TweenBuffer
{
TweenFrameDesc TweenFrames;
};
이걸 넣어준다.
TweenFrames정보가 들어가서, 이 정보를 토대로 적절하게 골라서 사용할 예정이다.
3. 17. TweenDemo.fx 쉐이더에서 GetAnimationMatrix를 수정하여 두 개의 애니메이션 보간을 적용 행렬을 구하게 하기
GetAnimationMatrix를 수정한다.
이전 시간에는 frame이 2개로 되어 보간을 했었는데
이번에는 애니메이션이 2개가 된다.
애니메이션 사이의 frame도 보간해야 되지만 이제 애니메이션 끼리도 보간해야 한다.
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[2];
int currFrame[2];
int nextFrame[2];
float ratio[2];
animIndex[0] = TweenFrames.curr.animIndex;
currFrame[0] = TweenFrames.curr.currFrame;
nextFrame[0] = TweenFrames.curr.nextFrame;
ratio[0] = TweenFrames.curr.ratio;
animIndex[1] = TweenFrames.next.animIndex;
currFrame[1] = TweenFrames.next.currFrame;
nextFrame[1] = TweenFrames.next.nextFrame;
ratio[1] = TweenFrames.next.ratio;
float4 c0, c1, c2, c3; // current
float4 n0, n1, n2, n3; // next
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.tweenRatio);
}
transform += mul(weights[i], result);
}
return transform;
}
shader는 더려워지긴 했지만 어렵진 않다.
shader가 점점 커지면 깔끔하게 관리하는 게 어렵다.
shader가 늘어나더라도 여러가지 pass를 줘서 여러 옵션에 따라 관리를 할 수 있게 만들어야 되는지 이런 것도 고민이다.
shader 코드만 보면 어려운 것은 없을 것이다.
Clinet를 빌드한다.
4. RenderManager.h에서 struct TweenDesc 정의하기
이제 ModelAnimator::Update에 가서 바꿔준다.
복습을 위해 복붙을 하고 원래 버전은 주석 처리를 한다.
마지막 버전은 트윈과 관련된 부분이라고 볼 수 있다.
RenderManager.h에서
struct TweenDesc
{
// 꼼꼼하게 안챙기면 엉뚱하게 돌아갈 수 있으니까 생성자를 만들어 초기값을 넣어준다.
TweenDesc()
{
curr.animIndex = 0;
next.animIndex = -1;
}
// 경우에 따라서 다음 애니메이션이 다 완료되었습니다. 라는 함수를 파줘서 그걸 호출하면 좋을 거 같으니까
// 편리하게 초기값으로 밀어주는 함수
void ClearNextAnim()
{
next.animIndex = -1;
next.currFrame = 0;
next.nextFrame = 0;
next.sumTime = 0;
tweenSumTime = 0;
tweenRatio = 0;
}
float tweenDuration = 1.0f;
float tweenRatio = 0.f; // 이전 상태에서 다음 상태로 넘어갈 때의 비율
float tweenSumTime = 0.f; // 경과된 시간
float padding = 0.f;
KeyframeDesc curr;
KeyframeDesc next;
};
이 struct를 추가한다.
5. RnderManager에서 desc와 buffer를 생성하고 버퍼와 shader의 버퍼변수와 연결 후 PushTweenData함수를 정의하기
RenderManager에서 추가하는 작업을 해야 한다.
RenderManager.h에서
void PushTweenData(const TweenDesc& desc);
TweenDesc _tweenDesc;
shared_ptr<ConstantBuffer<TweenDesc>> _tweenBuffer;
ComPtr<ID3DX11EffectConstantBuffer> _tweenEffectBuffer;
를 선언하고
RenderManager .cpp로 가서
Init에서
_tweenBuffer = make_shared<ConstantBuffer<TweenDesc>>();
_tweenBuffer->Create();
_tweenEffectBuffer = _shader->GetConstantBuffer("TweenBuffer");
을 추가한다.
슬슬 한계가 오고 있다. shader가 바뀔 때마다 이 작업을 매번 할 수 없으니 바꾸긴 해야 한다. 이 RenderManager가 전역으로 하나만 있으면 안 되기 때문에, 글로벌하게 사용하는 쉐이더 하나만 이걸 사용하거나 해야 하는데 지금은 흐름 자체가 관리를 RenderManager에 모두 밀어 넣는 작업을 해서 쉐이더에 넘기는 건 여기서 하는 걸로 구분을 하고 있으니까 조그만 이거로 버텨 본다.
void RenderManager::PushTweenData(const TweenDesc& desc)
{
_tweenDesc = desc;
_tweenBuffer->CopyData(_tweenDesc);
_tweenEffectBuffer->SetConstantBuffer(_tweenBuffer->GetComPtr().Get());
}
여기선 frame과 관련된 부분들을 받아서 밀어 넣는 작업을 해주게 된다.
6. ModelAnimator::Update를 TweenDesc와 PushTweeenData 버전으로 수정하기
ModelAnimator::Update에서 이제 KeyframeDesc이 아닌 TweenDesc로 작업을 해야 한다.
ModelAnimator.h에
TweenDesc _tweenDesc;
를 선언하고,
ModelAnimator::Update에서
TweenDesc& desc = _tweenDesc;
이렇게 참조변수에 대입해서 desc를 수정하면 _tweenDesc가 수정되게 한다.
현재 애니메이션을 뭔가 바꿔줘야 되니까
desc.curr.sumTime += DT;
이런 식으로 desc.curr에 무언가가 들어가게 해줘야 한다.
그리고 _keyframeDesc를 사용한 부분을 삭제한다.
_keyframeDesc.sumTime += DT; // Time Manager에서 Delta Time을 추출하여 경과된 시간(초)을 얻는 방식입니다.
shared_ptr<ModelAnimation> current = _model->GetAnimationByIndex(_keyframeDesc.animIndex);
if (current)
{
float timePerFrame = 1 / (current->frameRate * _keyframeDesc.speed); // 1프레임 당 경과되어야 하는 시간
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); // 경과된 시간/1프레임당시간 -> 0~1사이를 왔다갔다 하면서 이어지게 될 것이다.
}
이 부분을 삭제하고
// Anim Update
ImGui::InputInt("AnimIndex", &_keyframeDesc.animIndex);
_keyframeDesc.animIndex %= _model->GetAnimationCount();
ImGui::InputFloat("Speed", &_keyframeDesc.speed, 0.5f, 4.f);
// 애니메이션 현재 프레임 정보
RENDER->PushKeyframeData(_keyframeDesc);
이 코드도
// Anim Update
ImGui::InputInt("AnimIndex", &desc.curr.animIndex);
_keyframeDesc.animIndex %= _model->GetAnimationCount();
ImGui::InputFloat("Speed", &desc.curr.speed, 0.5f, 4.f);
// 애니메이션 현재 프레임 정보
RENDER->PushTweenData(desc);
이렇게 curr과 PushTweenData를 사용하는 버전으로 바꿔준다.
7. ModelAnimator::Update에 animation을 tweening하는 작업을 해서 _tweenDesc를 세팅하는 부분을 구현하기
void ModelAnimator::Update()
{
if (_model == nullptr)
return;
// TODO
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;
}
}
한마디로 이 코드는 현재 재생 중인 애니메이션의 프레임을 업데이트하고, 예약된 다음 애니메이션으로의 부드러운 전환을 관리하고 있다.
8. ModelAnimator::Update에 animation tweening을 테스트할 수 있게 ImGui를 세팅하기
그리고 다음 애니메이션으로 넘어가는 게 있어야 하니까
다른 애니메이션을 골라주기 위한 코드를 하나 넣어준다.
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();
InputInt의 반환값이 bool인데 왜 bool을 받아줄까?
버튼을 눌렀을 때 {}이 안에 들어오게 하기 위해서다.
9. 실행하기
실행을 하면
달리다 멈추거나 멈추다가 달릴 때 자연스럽게 전환하게 된다.
이런 건 엔진에서는 거의 기본으로 트렌지션 타임이라고 되어 있는 부분으로 제공된다.
이런 거는 사실 티가 많이 나진 않는다.
이렇게 애니메이션 관련해서 중요한 건 다 다뤘다.
물론 더 있긴 하지만 포폴 만들 때는 이 정도만 알아도 충분하다.
이렇게 두 개의 애니메이션을 섞어서 변환되게
struct TweenDesc에서 초기값으로 1초 안에 변하게 수치를 해 놓았지만
float tweenDuration을 조절해서 애니메이션이 변환되는 시간 조절이 가능하다.
쉐이더에 뭔가 연동시키는 게 쉽지 않다.
코드 만들고, 쉐이더 만들고 뭔가 밀어 넣는 코드를 만들고 하다 보니까 쉽지 않지만
새로운 아이디어가 나오면 연구해 보는 것은 지장 없이 할 수 있을 것이다.
'DirectX' 카테고리의 다른 글
66_인스턴싱_인스턴싱과 드로우콜 (0) | 2024.03.12 |
---|---|
65. 애니메이션_SkyBox (0) | 2024.03.10 |
63. 애니메이션_애니메이션#3_shader에서 애니메이션 행렬로 연산, ImGUI로 테스트, 보간 (0) | 2024.03.08 |
62. 애니메이션_애니메이션#2_CreateAnimationTransform, CreateTexture (0) | 2024.03.06 |
61. 애니메이션_애니메이션#1_ReadAnimation, ModelAnimator (0) | 2024.03.05 |
댓글