본문 바로가기

DirectX11 3D/기본 문법

<DirectX11 3D> 49 - Animation(Blending)

 

필요한 개념


언리얼은 메터리얼이 전부 shader로 이루어져서 모델 import시에 shader 컴파일이 일어난다.
Mixamo모델을 로드 하면 반투명 캐릭터로 보이는데 디자이너가 그렇게 설정했다.
그래서 불투명(opaque)로 바꿔주면 된다.
material 편집기 들어가서 blendMode->Opaque로 세팅하면 된다.

unreal은 .fbx파일에 texture를 압축을 푼다.
우리는 .fbx 내부에 압축되어 있는 texture를 압출 풀어서 복사해서 따로 파일로 저장했다.

unreal Animation 임포트 해서 보면 프레임마다 본의 값이 변하는 것을 볼 수 있다.

모델 언리얼 임포트 할때 설명 :
fbx File Information에 보면
File Creator(뭘로 부터 만들어졌냐), File Axis Direction(ex) Z-UP(RH) -> z축을 up으로 사용하겠다.)
max도 언리얼도 좌표가 DX랑 다름

언리얼은 좌표가 다르다. x : 전방, y : 우측, z : 수직

Animaton Start Frame, End Frame(프레임 시작과 끝)

 

BlendSpace1D 설명

 


BlendSpace1D : 기본 값에 의해서 움직일 수 있도록 함
(우리가 사용하는 것은 플레이어의 속도에 따라 동작을 결정)

if문으로 하면 뚝뚝끊기니, 부드럽게 처리하는 blend기능을 사용한다.

우리는 0 ~ 2까지 해서 idle, walk, run 구현할 것이다.

- Blending : 애니메이션을 동작을 섞어서(ex) 커피 블랜딩)

세팅할 Blending 구조

 

 

 

 

ModelAnimator.h 추가된 부분


더보기
class ModelAnimator
{
	...
private:
	void UpdateBlendMode();
    
public: 
	void PlayBlendMode(UINT clip, UINT clip1, UINT clip2);
	void SetBlendAlpha(float alpha);
    
private:
	struct BlendDesc
	{
		// 0보다 크면 Blend모드로 수행
		// 0이라면 TweenMode로 수행
		UINT Mode = 0;
		// 언리얼의 speed로 움직인 값과 동일한 역할
		// 얼마만큼 섞일지
		float Alpha = 0;
		Vector2 Padding;

		// 동작은 3개로 고정시킨다고 가정해서 사용(ex) idle, walk, run)
		// 필요하다면 더 많이 세팅해도 된다.
		KeyframeDesc Clip[3];
	}blendDesc;

	ConstantBuffer* blendBuffer;
	ID3DX11EffectConstantBuffer* sBlendBuffer;
}

 

 

 

 

 

 

 

ModelAnimator.Cpp 추가된 부분


더보기
ModelAnimator::ModelAnimator(Shader* shader) 
	: shader(shader)
{
	...
	blendBuffer = new ConstantBuffer(&blendDesc, sizeof(BlendDesc));
	sBlendBuffer = shader->AsConstantBuffer("CB_BlendFrame");
}

void ModelAnimator::Update()
{		
	// 0보다 크면 Blend모드로 수행
	// 0이라면 TweenMode로 수행
	
	if (blendDesc.Mode == 0)
		UpdateTweenMode();
	else
		UpdateBlendMode();
        
    	...
}

void ModelAnimator::UpdateBlendMode()
{
	// 이것도 비율로 만드는 것, 별로 복잡하지 x
	BlendDesc& desc = blendDesc;

	// 동작 총 3개(0, 1, 2)
	for (UINT i = 0; i < 3; i++)
	{
		ModelClip* clip = model->ClipByIndex(desc.Clip[i].Clip);

		desc.Clip[i].RunningTime += Time::Delta();

		float time = 1.0f / clip->FrameRate() / desc.Clip[i].Speed;

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

void ModelAnimator::PlayBlendMode(UINT clip, UINT clip1, UINT clip2)
{
	blendDesc.Mode = 1;

	blendDesc.Clip[0].Clip = clip;
	blendDesc.Clip[1].Clip = clip1;
	blendDesc.Clip[2].Clip = clip2;
}

void ModelAnimator::SetBlendAlpha(float alpha)
{
	// Clmap : 값 제한
	alpha = Math::Clamp(alpha, 0.0f, 2.0f);

	blendDesc.Alpha = alpha;
}

 

 

AnimationDemo.h


더보기
#pragma once
#include "Systems/IExecute.h"

class AnimationDemo : public IExecute
{
public:
	virtual void Initialize() override;
	virtual void Ready() override {};
	virtual void Destroy() override {};
	virtual void Update() override;
	virtual void PreRender() override {};
	virtual void Render() override;
	virtual void PostRender() override {};
	virtual void ResizeScreen() override {};

private:
	void Kachujin();

private:
	Shader* shader;

	ModelAnimator* kachujin = nullptr;
};

 

AnimationDemo.cpp


더보기
#include "stdafx.h"
#include "AnimationDemo.h"
#include "Converter.h"

void AnimationDemo::Initialize()
{
	Context::Get()->GetCamera()->RotationDegree(17, 0, 0);
	Context::Get()->GetCamera()->Position(0, 6, -41);

	shader = new Shader(L"49_AnimationBlending.fx");

	Kachujin();
}

void AnimationDemo::Update()
{
	if (kachujin != nullptr) kachujin->Update();
}


void AnimationDemo::Render()
{
	//static float speed = 1.0f;
	//static float takeTime = 1.0f;
	//static int clip = 0;

	//static bool bBlendMode = false;
	//static float blendAlpha = 0.0f;
	//

	//ImGui::Checkbox("BlendMode", &bBlendMode);
	//if (bBlendMode == false)
	//{
	//	ImGui::InputInt("Clip", &clip);
	//	clip %= 5;

	//	ImGui::SliderFloat("Speed", &speed, 0.1f, 5.0f);
	//	ImGui::SliderFloat("TakeTime", &takeTime, 0.1f, 5.0f);

	//	if (ImGui::Button("Apply"))
	//		kachujin->PlayTweenMode(clip, speed, takeTime);
	//}

	//else
	//{
	//	ImGui::SliderFloat("Alpha", &blendAlpha, 0.0f, 2.0f);

	//	kachujin->SetBlendAlpha(blendAlpha);

	//	if (ImGui::Button("Apply"))
	//		kachujin->PlayBlendMode(0, 1, 2);
	//}


	//ImGui::SliderFloat3("Direction", direction, -1, +1);
	//shader->AsVector("Direction")->SetFloatVector(direction);
	//
	//static int pass = 0;
	//ImGui::InputInt("Pass2", &pass);
	//pass %= 2;

	if (kachujin != nullptr)
		kachujin->Render();
}

void AnimationDemo::Kachujin()
{
	kachujin = new ModelAnimator(shader);
	kachujin->ReadMesh(L"Kachujin/Mesh");
	kachujin->ReadMaterial(L"Kachujin/Mesh");
	kachujin->ReadClip(L"Kachujin/Sword And Shield Idle");
	kachujin->ReadClip(L"Kachujin/Sword And Shield Walk");
	kachujin->ReadClip(L"Kachujin/Sword And Shield Run");
	kachujin->ReadClip(L"Kachujin/Sword And Shield Slash");
	kachujin->ReadClip(L"Kachujin/Salsa Dancing");
	kachujin->GetTransform()->Position(0, 0, -30);
	kachujin->GetTransform()->Scale(0.025f, 0.025f, 0.025f);
}

 

 

 

 

 

 

 

AnimationBlending.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 SetTweenWorld(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);
}


struct BlendFrame
{
    uint Mode;
    float Alpha;
    float2 Padding;

    AnimationFrame Clip[3];
};

cbuffer CB_BlendFrame
{
    // s는 인스턴싱 때문에 미리 붙여놓음
    BlendFrame BlendFrames;
};

void SetBlendWorld(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 };


    // 행단위로 써놨었다. 읽어들임
    float4 c0, c1, c2, c3;
    float4 n0, n1, n2, n3;
    
    
    matrix curr = 0, next = 0;
    matrix currAnim[3];
    matrix anim = 0;
    
    // 최종행렬
    matrix transform = 0;
    
    // 현재 동작 구해줌
    [unroll(4)]
    for (int i = 0; i < 4; i++)
    {
        [unroll(3)]
        for (int k = 0; k < 3; k++)
        {
            c0 = TransformsMap.Load(int4(indices[i] * 4 + 0, BlendFrames.Clip[k].CurrFrame, BlendFrames.Clip[k].Clip, 0)); // 1행
            c1 = TransformsMap.Load(int4(indices[i] * 4 + 1, BlendFrames.Clip[k].CurrFrame, BlendFrames.Clip[k].Clip, 0)); // 2행
            c2 = TransformsMap.Load(int4(indices[i] * 4 + 2, BlendFrames.Clip[k].CurrFrame, BlendFrames.Clip[k].Clip, 0)); // 3행
            c3 = TransformsMap.Load(int4(indices[i] * 4 + 3, BlendFrames.Clip[k].CurrFrame, BlendFrames.Clip[k].Clip, 0)); // 4행
            curr = matrix(c0, c1, c2, c3);
        
            n0 = TransformsMap.Load(int4(indices[i] * 4 + 0, BlendFrames.Clip[k].NextFrame, BlendFrames.Clip[k].Clip, 0)); // 1행
            n1 = TransformsMap.Load(int4(indices[i] * 4 + 1, BlendFrames.Clip[k].NextFrame, BlendFrames.Clip[k].Clip, 0)); // 2행
            n2 = TransformsMap.Load(int4(indices[i] * 4 + 2, BlendFrames.Clip[k].NextFrame, BlendFrames.Clip[k].Clip, 0)); // 3행
            n3 = TransformsMap.Load(int4(indices[i] * 4 + 3, BlendFrames.Clip[k].NextFrame, BlendFrames.Clip[k].Clip, 0)); // 4행
            next = matrix(n0, n1, n2, n3);
     
            currAnim[k] = lerp(curr, next, BlendFrames.Clip[k].Time);
        }
        
        // 0, 1, 2 구간이 떨어짐
        int clipA = (int) BlendFrames.Alpha;
        int clipB = clipA + 1;
        
        float alpha = BlendFrames.Alpha;
        
        if(alpha >= 1.0f)
        {
            // 1 ~ 2구간을 가야하니까 -1을 시킴
            // lerp를 하기 위해 0 ~ 1구간을 만들어야 해서 -1을 함
            alpha = BlendFrames.Alpha - 1.0f;
            
            // 2를 넘었다면 플레이 구간이 1 ~ 2이다.
            if(BlendFrames.Alpha >= 2.0f)
            {
                clipA = 1;
                clipB = 2;
            }
        }
        
        anim = lerp(currAnim[clipA], currAnim[clipB], alpha);      
       
        transform += mul(weights[i], anim);
    }
    world = mul(transform, world);
}

VertexOutput VS(VertexModel input)
{
    VertexOutput output;
    
    //World = mul(BoneTransforms[BoneIndex], World);
    //SetTweenWorld(World, input);
   
    if(BlendFrames.Mode == 0)
        SetTweenWorld(World, input);
    else
        SetBlendWorld(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)
}

 

 

 

 

 

 

 

 

 

 

 

결과


- 언리얼 BlendSpace1D에서 Speed 변수로 Blending 해줬다면, DX에서는 Alpha값(0 ~ 2) 값으로 Blending 해줬다.