필요한 개념
- 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);
<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 : 현재 동작과 다음 동작을 보간하여, 동작 간의 전환을 부드럽게 해주는 방법
다른 사람 포폴 포면 한 동작(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)전환이 빨라지는 것을 알 수 있다.
-> 동작 전환이 빠를시에 어색함이 덜 한 것을 볼 수 있다.
'DirectX11 3D > 기본 문법' 카테고리의 다른 글
<DirectX11 3D> 50 - Instancing(인스턴싱) (0) | 2022.02.21 |
---|---|
<DirectX11 3D> 49 - Animation(Blending) (0) | 2022.02.21 |
<Directx11 3D> 46 - Animation Rendering (0) | 2022.02.17 |
<Directx11 3D> 42 - Animation Clip Read & Write (0) | 2022.02.16 |
<DirectX11 3D> 41 - Animation (Mesh Skinning) (0) | 2022.02.15 |