프로그래밍 공부
작성일
2023. 11. 23. 01:16
작성자
WDmil
728x90

함수포인터를 사용해서 애니메이션의 자동 프레임전환을 구현해보자.

 

언리얼에서 사용하는 애니메이션의 자동 전환은. 일정 시간 뒤에 애니메이션이 자동으로 전환되는 형식으로 구성되어있는데,

 

https://docs.unrealengine.com/4.27/ko/AnimatingObjects/SkeletalMeshAnimation/Sequences/

 

애니메이션 시퀀스

Animation Sequence, 애니메이션 시퀀스 애셋은 한 애니메이션을 이루는 모든 트랜스폼 데이터가 들어있는 개별 애셋입니다.

docs.unrealengine.com

 

이러한 애니메이션간 선형전환방식을 사용할것 이다.

 

class ModelClip
{
private:
	friend class ModelAnimator;

	ModelClip() = default;
	~ModelClip();

	void Init();
	void Excute();

	KeyFrame* GetKeyFrame(string boneName) { return keyFrames[boneName]; }

public:
	void SetEvent(Event event, float timeRatio) { events[timeRatio] = event; }

private:
	string name;

	UINT frameCount;
	float tickPerSecond, duration;
	float playTime = 0.0f;

	// 노드 이름을 키로 하고 프레임 데이터를 데이터로 한다.
	unordered_map<string, KeyFrame*> keyFrames;

	map<float, Event> events;
	map<float, Event>::iterator eventIter;
};

 

우선 애니메이션 클립 저장 객체에 map형태의 event 변수를 생성한다.

 

map은 진입된 Key값의 순서대로 저장되는 성질이 있음으로. 더 빠른 위치의 애니메이션이 입력되었을 때, 그 애니메이션이 출력될것이다.

 

0초에 애니메이션을 지정하고, 0.5초 뒤에 Run이라는 애니메이션을 집어넣었다면,

 

float의 시간대로 Map을 검색하고, count시, 1이상이 검색되었을 경우, 그 애니메이션의 전환 event를 실행시켜주면 된다.

 

void ModelClip::Init()
{
    duration = frameCount / tickPerSecond;
    eventIter = events.begin();
    playTime = 0.0f;
}

void ModelClip::Excute()
{
    if (events.empty()) return;
    if (eventIter == events.end()) return;

    float ratio = playTime / duration;

    if (eventIter->first > ratio) return;

    eventIter->second();
    eventIter++;
}

 

현재 eventIter에서 검색되어지지 않았을 경우, 또는 마지막일경우, 반환시켜준다. 실행을 무시한다.

 

만약, 전부다 해당되면서, 현재 애니메이션 재생위치가 처음보다 클경우,(음수진입이 안되었을경우) 

 

Iter에 지정되어있는 Event를 실행시키고, Iter를 다음으로 넘겨준다.

 

	void PlayClip(int clip, float scale = 1.0f, float takeTime = 0.2f);

	// 텍스처 생성 함수: 텍스처를 생성하여 저장
	void CreateTexture();

	Matrix GetTransformByNode(int nodeIndex);

	void SetRunAnimation(bool input) { IsPlay = input; }
	ModelClip* GetClip(UINT clip) { return clips[clip]; }

protected:
	// 클립 변환 생성 함수: 지정된 인덱스에 대한 클립 변환 매트릭스 생성
	void CreateClipTransform(UINT index);
	void UpdateFrame();

 

ModelAnimator에 GetTransformByNode과 PlayClip을 생성해준다.

 

Matrix ModelAnimator::GetTransformByNode(int nodeIndex)
{

    Matrix curAnim;

    {
        Frame& frame = frameBuffer->GetData()->cur;

        Matrix cur = nodeTransforms[frame.clip].transform[frame.curFrame][nodeIndex];
        Matrix next = nodeTransforms[frame.clip].transform[frame.curFrame + 1][nodeIndex];

        curAnim = MATH->Lerp(cur, next, frame.time) * world;
    }

    {
        Frame& frame = frameBuffer->GetData()->next;

        if (frame.clip < 0)
            return curAnim;

        Matrix cur = nodeTransforms[frame.clip].transform[frame.curFrame][nodeIndex];
        Matrix next = nodeTransforms[frame.clip].transform[frame.curFrame + 1][nodeIndex];

        Matrix nextAnim = MATH->Lerp(cur, next, frame.time) * world;

        return MATH->Lerp(curAnim, nextAnim, frameBuffer->GetData()->tweenTime);
    }
}

 

ModelAnimator은, 지정된 AnimClip객체에서 현재 재생되고있는 애니메이션의 클립데이터의 보간정보값을 받아와서.

 

Node와 Node사이의 전환데이터의 중간 Matrix를 받아온다.

 

이 Matrix는, 객체에 상속된 Obejct가 존재할경우, 해당 Object의 위치정보 변환에 사용된다.

 

void ModelAnimator::PlayClip(int clip, float scale, float takeTime)
{
    IsPlay = true;

    frameBuffer->GetData()->next.clip = clip;
    frameBuffer->GetData()->next.scale = scale;
    frameBuffer->GetData()->takeTime = takeTime;

    clips[clip]->Init();
}

 

playClip에서는, 지정된 클립과 속도, 변환시간을 넣은다음. clip을 초기화시켜준다.

 

시작초기화를 하는것.

 

그리고,

void ModelAnimator::UpdateFrame()
{
    if (!IsPlay) return;

    Motion* motion = frameBuffer->GetData();

    {
        Frame* frame = &motion->cur;
        ModelClip* clip = clips[frame->clip];

        frame->time += clip->tickPerSecond * frame->scale * DELTA;
        clip->playTime += frame->scale * DELTA;

        if (frame->time >= 1.0f)
        {
            frame->curFrame = (frame->curFrame + 1) % (clip->frameCount - 1);
            frame->time -= 1.0f;
        }

        clip->Excute();
    }

    {
        Frame* frame = &motion->next;

        if (frame->clip < 0) return;

        ModelClip* clip = clips[frame->clip];

        motion->tweenTime += DELTA / motion->takeTime;

        if (motion->tweenTime >= 1.0f)
        {
            motion->cur = motion->next;
            motion->tweenTime = 0.0f;

            motion->next.clip = -1;
            motion->next.curFrame = 0;
            motion->next.time = 0.0f;
            return;
        }

        frame->time += clip->tickPerSecond * frame->scale * DELTA;

        if (frame->time >= 1.0f)
        {
            frame->curFrame = (frame->curFrame + 1) % (clip->frameCount - 1);
            frame->time -= 1.0f;
        }
    }
}

 

여기에 

 

clip->playTime += frame->scale * DELTA;

 

이러한 코드를 추가하여, 현재 play되고있는 시간에 DELTA초에다가 scale값을 곱한만큼 충첩연산을 시켜준다.

 

이것으로 현재 지속되고있는 시간정보를 알 수 있다.

 

class Traveler : public CapsuleCollider
{
private:
    enum ActionState
    {
        IDLE,
        ATTACK
    };

public:
    Traveler();
    ~Traveler();

    void Update();
    void Render();
    void GUIRender();

private:
    void Attack();
    void EndAttack();

    void SetState(ActionState state);

    void ReadClips();

private:
    ModelAnimator* bodyMesh;
    Model* sword;

    Transform* rightHand;

    ActionState curState;
};
#include "Framework.h"
#include "Traveler.h"

Traveler::Traveler()
{
    bodyMesh = new ModelAnimator("Traveler");
    bodyMesh->SetShader(L"Model/2DTextureModel.hlsl");
    bodyMesh->Load();
    bodyMesh->SetParent(this);
    ReadClips();

    rightHand = new Transform();

    sword = new Model("weapon0");
    sword->SetParent(rightHand);
    sword->Load();
}

Traveler::~Traveler()
{
    delete bodyMesh;
    delete sword;
    delete rightHand;
}

void Traveler::Update()
{
    Attack();

    rightHand->SetWorld(bodyMesh->GetTransformByNode(36));

    UpdateWorld();
    bodyMesh->Update();
    sword->UpdateWorld();
}

void Traveler::Render()
{
    bodyMesh->Render();
    sword->Render();
}

void Traveler::GUIRender()
{
    bodyMesh->GUIRender();
    sword->GUIRender();
}

void Traveler::Attack()
{
    if (KEY->Down(VK_LBUTTON))
    {
        SetState(ATTACK);
    }
}

void Traveler::EndAttack()
{
    SetState(IDLE);
}

void Traveler::SetState(ActionState state)
{
    if (curState == state) return;

    curState = state;
    bodyMesh->PlayClip(state);
}

void Traveler::ReadClips()
{
    bodyMesh->ReadClip("Idle");
    bodyMesh->ReadClip("Walk");
    bodyMesh->ReadClip("Flair");
    bodyMesh->CreateTexture();

    bodyMesh->GetClip(ATTACK)->SetEvent(bind(&Traveler::EndAttack, this), 0.8f);
}

 

애니메이션을 직접 실행해주고 변경해주는 객체를 생성한다.

 

애니메이션은 Idle과 Walk와 Flair로 지정하였으며,

 

마우스 왼쪽 버튼을 누르면, Walk가 실행되고, 0.8초 후에 Idle상태로 돌아온다.

 

 

시연영상. 마우스 왼쪽클릭을 하면, 걷는동작이 나오고 0.8초뒤에 정지한다.

728x90