본문 바로가기

DirectX11 3D/기본 문법

<DirectX11 3D> 48 - Animation(FrameLerp, Tween)

 

필요한 개념


- Running Time이란 ? 애니메이션이 현재 플레이 되고 있는 시간
RunningTime에 DeltaTime을 누적

문제 발생 1)
동작이 뚝뚝 끊긴다.
프레임 간의 보간이 필요-> Lerp가 필요
Lerp(0 ~ 1까지 범위로 처리한다.)

- FrameLerp : 현재 프레임과 다음 프레임의 사이를 보간해준다.
-> shader에서 lerp(현재 프레임 Matrix, 다음 프레임 Matrix, 0 ~ 1 값) 함수

 

shader에서
current c0, c1, c2, c3를 구했는데
Next도 n0, n1, n2, n3로 구해주고
두개의 프레임을 Lerp을 이용해 프레임간의 보간해준다.(각각의 Matrix를 활용해)

// (현재 프레임 Matrix, 다음 프레임 Matrix, 시간값(0 ~ 1))
currAnim = lerp(curr, next, time);

 

 

https://cpplab.tistory.com/47

 

<DirectX11 3D> Lerp

Lerp란? Linear Interpolation(선형 보간)의 약자이다. 두 값을 섞는다. 수식 Lerp(A, B, Alpha) = A x (1 - Alpha) + B x Alpha HLSL 내장 함수 lerp(x,y,s) : x값과 y값 사이의 중간값들을 구할 수 있다.

cpplab.tistory.com

 



Tweening 기법 설명



- Tweening : 현재 동작과 다음 동작을 보간하여, 동작 간의 전환을 부드럽게 해주는 방법

다른 사람 포폴 포면 한 동작(Clip)에서 다음 동작(Clip)으로 넘어갈때 뚝뚝 끊기는 것을 느꼈을 것이다.

동작을 바꿔가는 함수
TakeTime : 한 동작과 다음 동작의 전환 될때 까지 소요시간(동작 전환에 대한 소요시간)
Shader에 넘어갈 frameBuffer도 1개 더 있어야함, 현재 동작과 다음 동작을 동시에 실행시키기 위해(Clip(동작) 전환 간의 뚝 뚝 끊김 방지)
void PlayTweenMode(UINT clip, float speed = 1.0f, float takeTime = 1.0f);


이전에 애니메이션이 튀는 현상(clip이 바뀌어 가면서 검은색 화면이 생김)이 발생하지 않는다.
왜나면 이전 동작과 현재 동작에 차이가 있기 때문에

동작이 부드러워 진것을 볼 수 있다.

but 캐릭터가 울렁거리는 현상은 우리가 lerp로 중간값을 취하는 것이라 어쩔 수가 없는 것이다.(빠르게 플레이 시키면 티가 안남)

 

 

 

 

 

 

ModelAnimator.h 추가된 부분


더보기
class ModelAnimator
{
...

private:
	void UpdateTweenMode();
    
public:
	// 동작을 바꿔가는 함수
	// takeTime : 한 동작과 다음 동작의 전환 될때 까지 소요시간(동작 전환에 대한 소요시간)
	// Shader에 넘어갈 frameBuffer도 1개 더 있어야함, 현재 동작과 다음 동작을 동시에 실행시키기 위해(Clip(동작) 전환 간의 뚝 뚝 끊김 방지)
	void PlayTweenMode(UINT clip, float speed = 1.0f, float takeTime = 1.0f);
	

private:
	struct TweenDesc
	{
		// 애니메이션이 변화되는 시간
		float TakeTime = 1.0f;
		// 변해가는 시간 기록
		float TweenTime = 0.0f;
		float ChangeTime = 0.0f;

		// Padding이 없으면 넣을 구조체가 땡겨져 버려서 넣음 
		float Padding;


		// lerp 하기 위한
		// 
		// 현재 동작
		KeyframeDesc Curr;
		// 다음 동작
		KeyframeDesc Next;

		TweenDesc()
		{
			Curr.Clip = 0;
			// -1은 clip이 없다는 뜻
			Next.Clip = -1;
		}
	} tweenDesc;
}

 

 

 

 

 

 

 

ModelAnimator.Cpp 추가된 부분


더보기
ModelAnimator::ModelAnimator(Shader* shader) 
	: shader(shader)
{
	...
    	frameBuffer = new ConstantBuffer(&tweenDesc, sizeof(TweenDesc));
	sFrameBuffer = shader->AsConstantBuffer("CB_TweenFrame");
}

void ModelAnimator::Update()
{			
	UpdateTweenMode();
    	...
}

void ModelAnimator::UpdateTweenMode()
{
	// 시간에 따라 애니메이션 플레이
	// 이럴때는 비율을 만드는게 중요(시간비율)
	TweenDesc& desc = tweenDesc;

	// 현재 애니메이션
	{
		ModelClip* clip = model->ClipByIndex(desc.Curr.Clip);

		// 이전 프레임과 현재 프레임의 차인 델타 타임을 누적시킨다.
		// Running Time이란 ? 애니메이션이 현재 플레이 되고 있는 시간
		desc.Curr.RunningTime += Time::Delta();

		// 시간 비율 생성
		// FrameRate가 30일 것이다. 그러면 30분의 1
		// * desc.Speed가 맞기는 한데 조정편하게 하라고 / 함
		float time = 1.0f / clip->FrameRate() / desc.Curr.Speed;

		// 애니메이션의 다음 프레임으로 넘긴다.(시간을 0 ~ 1까지 만든 것을)
		if (desc.Curr.Time >= 1.0f)
		{
			desc.Curr.RunningTime = 0;
			// 순환 시킴(루프 처리)
			// 한번만 플레이하고 싶다면 마지막 프레임에서 멈추면 됨
			desc.Curr.CurrFrame = (desc.Curr.CurrFrame + 1) % clip->FrameCount();

			//문제 발생 1)
			//동작이 뚝뚝 끊긴다.
			//프레임 간의 보간이 필요->Lerp가 필요
			//Lerp(0 ~1까지니까)
			desc.Curr.NextFrame = (desc.Curr.CurrFrame + 1) % clip->FrameCount();
		}
		// 비율을 RunningTime에다가 나눠준다.
		desc.Curr.Time = desc.Curr.RunningTime / time;

	}

	// 다음 동작 플레이(-1보다 큰 거면 다음 동작이 있다는 뜻)
	if (desc.Next.Clip > -1)
	{
		// TweenTime 계산
		desc.ChangeTime += Time::Delta();
		// 0 ~ 1까지 정규화된 시간을 갖는다.
		desc.TweenTime = desc.ChangeTime / desc.TakeTime;


		// TweenTime이 1보다 크다면 Animation 전환이 완료 되었다는 얘기다.
		if (desc.TweenTime >= 1.0f)
		{
			// 초기화 해줌
			desc.Curr = desc.Next;

			desc.Next.Clip = -1;
			desc.Next.CurrFrame = 0;
			desc.Next.NextFrame = 0;
			desc.Next.Time = 0;
			desc.Next.RunningTime = 0.0f;

			desc.ChangeTime = 0.0f;
			desc.TweenTime = 0.0f;
		}

		// 완료되지 않은 상황(변화되어가는 상황)
		else
		{
			ModelClip* clip = model->ClipByIndex(desc.Next.Clip);

			desc.Next.RunningTime += Time::Delta();

			float time = 1.0f / clip->FrameRate() / desc.Next.Speed;

			if (desc.Next.Time >= 1.0f)
			{
				desc.Next.RunningTime = 0;
				desc.Next.CurrFrame = (desc.Next.CurrFrame + 1) % clip->FrameCount();
				desc.Next.NextFrame = (desc.Next.CurrFrame + 1) % clip->FrameCount();
			}
			desc.Next.Time = desc.Next.RunningTime / time;
		}
	}

}

void ModelAnimator::PlayTweenMode(UINT clip, float speed, float takeTime)
{	
	tweenDesc.TakeTime = takeTime;

	tweenDesc.Next.Clip = clip;
	tweenDesc.Next.Speed = speed;
}

 

 

 

 

 

 

 

 

AnimationTweening.fx


더보기
#include "00_Global.fx"

float3 Direction = float3(-1, -1, 1);

struct VertexModel
{
    float4 Position : Position;
    float2 Uv : Uv;
    float3 Normal : Normal;
    float3 Tangent : Tangent;
    float4 BlendIndices : BlendIndices;
    float4 BlendWeights : BlendWeights;
};

#define MAX_MODEL_TRANSFORMS 250
#define MAX_MODEL_KEYFRAMES 500

cbuffer CB_Bone
{
    matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
    
    uint BoneIndex;
    
    // CBuffer 전체가 배열이 되지 않는한, padding은 안 넣어줘도 된다.
};

struct AnimationFrame
{
    int Clip;

    uint CurrFrame;
    uint NextFrame;

    float Time;
    float Running;

    float3 Padding;
};

struct TweenFrame
{
    float TakeTime;
    float TweenTime;
    float RunningTime;
    float Padding;

    AnimationFrame Curr;
    AnimationFrame Next;
};

// 묶어서 전역변수로 쓰기 위해
cbuffer CB_TweenFrame
{
    TweenFrame TweenFrames;
};

Texture2DArray TransformsMap;

struct VertexOutput
{
    // SV_Position -> position0번으로 취급됨
    float4 Position : SV_Position;
    float3 Normal : Normal;
    float2 Uv : Uv;
};

// inout : 입력, 출력용으로 사용하겠다.
// in : 입력용으로 사용하겠다. out : 출력용으로 사용하겠다.
// Animation의 world를 계산함
// world는 입력도 하고 출력도 할거라서 inout
void SetAnimationWorld(inout matrix world, VertexModel input)
{
    // 각 항목을 for문으로 다루기 위해 배열로 저장
    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 };

    
    // 0번이 현재 동작 1번이 다음동작(Tweening 때문에)
    // 이후 작업 편하게 하려고 변수 선언
    int clip[2];
    int currFrame[2];
    int nextFrame[2];
    float time[2];
    
    clip[0] = TweenFrames.Curr.Clip;
    currFrame[0] = TweenFrames.Curr.CurrFrame;
    nextFrame[0] = TweenFrames.Curr.NextFrame;
    time[0] = TweenFrames.Curr.Time;
    
    clip[1] = TweenFrames.Next.Clip;
    currFrame[1] = TweenFrames.Next.CurrFrame;
    nextFrame[1] = TweenFrames.Next.NextFrame;
    time[1] = TweenFrames.Next.Time;

    // 행단위로 써놨었다. 읽어들임
    float4 c0, c1, c2, c3;
    float4 n0, n1, n2, n3;
    
    
    matrix curr = 0, next = 0;
    matrix currAnim = 0;
    matrix nextAnim = 0;
    
    // 최종행렬
    matrix transform = 0;
    
    // 현재 동작 구해줌
    [unroll(4)]
    for (int i = 0; i < 4; i++)
    {
        // 가중치가 4개였다.(스키닝을 하기위한)
        // Sample은 확대/축소가 일어나므로, 데이터의 변형이 일어나서
        // 사용할 수 없다. 그래서 Load을 사용 -> 행열로 원본데이터를 불러옴
        // int4 ? 왜 int인가? -> 마지막 w는 밉맵의 번호이다, 우리는 맵맵을 한장으로 사용하므로 0(우리는 민맵 1로 하나만 사용하니까 0을 줌)
        // 4개 픽셀 했었어서 *4 곱함(x는 그렇다.)
        // + 0은 각 픽셀의 0행을 위해
        // 본 번호(열), 키 프레임(행), clip(면)
        c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, currFrame[0], clip[0], 0)); // 1행
        c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, currFrame[0], clip[0], 0)); // 2행
        c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, currFrame[0], clip[0], 0)); // 3행
        c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, currFrame[0], clip[0], 0)); // 4행
        curr = matrix(c0, c1, c2, c3);
        
        n0 = TransformsMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], clip[0], 0)); // 1행
        n1 = TransformsMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], clip[0], 0)); // 2행
        n2 = TransformsMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], clip[0], 0)); // 3행
        n3 = TransformsMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], clip[0], 0)); // 4행
        next = matrix(n0, n1, n2, n3);
        
        // 이전 프레임에서 현재 프레임까지의 갑시, desc.time은 0 ~ 1 범위이므로 lerp의 보간 값으로 그대로 대입한다.
        // lerp도 0 ~ 1값으로 처리하기 때문이다.
        // (현재 프레임 Matrix, 다음 프레임 Matrix, 시간값(0 ~ 1))
        currAnim = lerp(curr, next, time[0]);
        
        
        
        // 다음 동작 구해줌
        // flatten : 제어문법(현재는 신경쓰지 말자)
        // 다음이 -1 보다 크다면(clip이 있다는 뜻) 다음 동작을 플레이 시킨다.
        [flatten]
        if(clip[1] > -1)
        {
            c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, currFrame[1], clip[1], 0)); // 1행
            c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, currFrame[1], clip[1], 0)); // 2행
            c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, currFrame[1], clip[1], 0)); // 3행
            c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, currFrame[1], clip[1], 0)); // 4행
            curr = matrix(c0, c1, c2, c3);
        
            n0 = TransformsMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], clip[1], 0)); // 1행
            n1 = TransformsMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], clip[1], 0)); // 2행
            n2 = TransformsMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], clip[1], 0)); // 3행
            n3 = TransformsMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], clip[1], 0)); // 4행
            next = matrix(n0, n1, n2, n3);
            
            nextAnim = lerp(curr, next, time[1]);
            
            // TweenTime : 0 ~ 1까지 정규화되어 있다.
            currAnim = lerp(currAnim, nextAnim, TweenFrames.TweenTime);
        }
        
        
        // 최종행렬에 가중치만 누적시키면 된다.
        // 곱한걸 계속 누적시키는 것(계속 영향을 받을 수 있도록)
        // 초반에 설명한 weights[i] 0.5라면 0.5만큼 curr에 곱해줌(움직여줌)
        // transform : 가중치, 프레임까지 결합된 행렬
        transform += mul(weights[i], currAnim);
    }
    
    // 최종행렬을 world로 변환
    // transform : 애니메이션이 이동할 행렬, world : 모델이 출력할 world
    world = mul(transform, world);
}

VertexOutput VS(VertexModel input)
{
    VertexOutput output;
    
    //World = mul(BoneTransforms[BoneIndex], World);
    SetAnimationWorld(World, input);
    
    // ModelMesh의 Transform이 넘어오나, Bone의 위치로 옮긴 다음, World로 옮겨야함
    output.Position = WorldPosition(input.Position);
    output.Position = ViewProjection(output.Position);
    
    output.Normal = WorldNormal(input.Normal);
    output.Uv = input.Uv;
    
    return output;
}

float4 PS(VertexOutput input) : SV_Target
{    
    // 음영 넣기 위해
    float NdotL = dot(normalize(input.Normal), -Direction);
    
    return DiffuseMap.Sample(LinearSampler, input.Uv) * NdotL;
}

technique11 T0
{
    P_VP(P0, VS, PS)
    P_RS_VP(P1, FillMode_WireFrame, VS, PS)
}

 

 

 

 

 

 

 

 

 

 

 

결과


프레임간의 많이 부드러워진 것을 알 수 있다. -> FrameLerp

 

Clip(동작)간의 많이 부드러워진 것을 알 수 있다. -> Tweening

 

코드 내용 중

=====================================================
desc.ChangeTime += Time::Delta();
desc.TweenTime = desc.ChangeTime / desc.TakeTime;

이 수식을 통해 TakeTime이 늘어날시에 동작(clip)전환이 빨라지는 것을 알 수 있다.

-> 동작 전환이 빠를시에 어색함이 덜 한 것을 볼 수 있다.