드디어 애니메이션을 실행시킨다. 언리얼의 애니메이션 기능을 하나씩 붙여보려고 한다. 해당 문서에서는 AnimSequence 실행과 RootMotion 적용을 다룬다.
0번으로 스킨 메시가 렌더링 되어야한다. 이미 이전에 렌더링되도록 만든것 아닌가라는 의문이 든다면 스킨 메시에 대해서 공부해보자.
스켈레탈 메시는 아래 사진 우측과 같이 본이 트리 구조로 이루어져있다. 본은 부모본에 Attach된 상태라고 인지하면 쉽다. 부모 본에 상대적인 위치를 가지고 있어 부모 본의 Transform에 변형이 가해진다면 자식 본들도 상태 Transform이 변경된다.
또한 이러한 본에 의해 메시 버텍스들이 움직이게 된다.
메시 버텍스 하나 당 N개의 본의 영향을 받을 수 있고 가중치가 설정되어 있다. 이거 때문에 스킨메시의 버텍스 포지션 구하는 방법이다른 것이다!

스켈레탈 컴포넌트와 프록시의 로직에서 BoneMatrix를 추가하고 VS를 다른 쉐이더를 사용하도록 처리했다.
아래는 본인덱스(4개 사용)에 따라 매트릭스를 계산해 버텍스 포지션 설정하는 것이다.
// 매트릭스 계산
float4x4 MakeSkinMatrix(uint4 idx, float4 w)
{
return gBones[idx.x] * w.x +
gBones[idx.y] * w.y +
gBones[idx.z] * w.z +
gBones[idx.w] * w.w;
}
// VS
VSOut VSMain(VSIn vin)
{
VSOut o;
float4x4 skin = MakeSkinMatrix(vin.boneIdx, vin.weight);
float3 skPos = mul(float4(vin.pos, 1), skin).xyz;
float3 skN = mul(vin.n, (float3x3)skin);
float3 skT = mul(vin.tangent.xyz, (float3x3)skin);
float4 posW = mul(float4(skPos,1), gWorld);
o.pos = mul(float4(skPos,1), gWorldViewProj);
o.posWS = posW.xyz;
float3x3 world3 = (float3x3)gWorld;
float3x3 normalMat = transpose(Inverse3x3(world3));
float3 N = normalize(mul(skN, normalMat));
float3 T = normalize(mul(skT, world3));
T = normalize(T - N * dot(N,T));
float sign = vin.tangent.w;
o.normalWS = N;
o.tangentWS = float4(T, sign);
o.bitanWS = normalize(cross(N,T)) * sign;
o.uv = vin.uv;
return o;
}
일단 FBX 파일 중 애니메이션이 들어있는 파일을 AnimSequenceAsset으로 임포트한 뒤 사용하였다.
// 본별 Transform 보간 -> 행렬로 변환하여 LocalPose에 저장
for (uint32_t b = 0; b < m_BoneCount; ++b)
{
const Transform& k0 = sec.Keys[(size_t)f0 * m_BoneCount + b];
const Transform& k1 = sec.Keys[(size_t)f1 * m_BoneCount + b];
Transform blended = Transform::Lerp(k0, k1, alpha);
DirectX::XMFLOAT4X4 M;
DirectX::XMStoreFloat4x4(&M, XMMatrixTranspose(blended.ToMatrix()));
m_LocalPose[b] = M;
}
void SkeletalMeshComponent::BuildFinalPalette_FromLocalPose(const std::vector<DirectX::XMFLOAT4X4>& localPose)
{
//.. 생략
// 로컬 Transform을 월드 Transform으로 변경
BuildGlobalPose_FromLocalPose(localPose);
// FinalPalette
for (uint32_t i = 0; i < n; ++i)
{
const auto& off = skel.Bones[i].Offset;
m_FinalPalette[i] = MulM(MulM(skel.GlobalInverse, m_GlobalPose[i]), off);
XMStoreFloat4x4(&m_FinalPalette[i], XMLoadFloat4x4(&m_FinalPalette[i]));
}
}
끼얏호우 잘동작한다 !!
사실 이렇게 바로 잘 동작하지 않았고 Matrix 때문에 꼬여서 디버깅을 많이했다. FBX는 Col-Major를 사용해서 matrix를 transpose해서 가져오고 있다. 이때, 어디까지 Col-Major를 사용하고 어디까지 Row-Major를 사용할지에 대해 정확하게 정하지 않은 상태에서 작업을 하다보니 한쪽에서 매트릭스가 꼬이는 사태가 발생했고 이를 디버깅하기 매우 어려웠다...(사실 코드 보고 알았다)
다음부터 작업할 땐 꼭 해당 정책을 정해야 겠다고 다짐했다 또한 Transpose하는 부분에선 꼭 이유를 적어둬야겠다. 내 코드니깐 알아보지 다른사람 작업이었으면 못찾았을 것 같다.

성질이 급해서 movementComponent도 없으면서 RootMotion을 구현했다... 일단 물리는 생각하지 않고 RootComponent의 Transform 변경해주는 기능만 만들어뒀다.
캐릭터의 머티리얼이 날아간 이유는... 믹사모 캐릭터에는 루트본이 없다.... 그래서 언리얼에서 루트본 추가하고 FBX로 Export하니깐 머티리얼이 다 날아가있다... 일단 이 이슈는 나중에 보기로하고 작업을 진행했다.
루트 모션을 실행하기 위해선 RootBone의 Transform을 본에서 제거해줘야한다. (RootComp에서 소모하기 위함)
void AnimInstance::ApplyRootLockToLocalPose(const AnimSection& sec)
{
//...생략
switch (sec.RootLockMode)
{
case ERootLockMode::RefPose:
if (root < m_Skeleton->RefLocalPose.size())
m_LocalPose[root] = m_Skeleton->RefLocalPose[root];
break;
case ERootLockMode::AnimFirstFrame:
if (!m_bHasFirstRootInSection)
{
m_FirstRootInSection = SampleBoneTransform(sec, root, 0.f);
m_bHasFirstRootInSection = true;
}
XMStoreFloat4x4(&m_LocalPose[root], m_FirstRootInSection.ToMatrix());
break;
case ERootLockMode::Zero:
default:
XMStoreFloat4x4(&m_LocalPose[root], XMMatrixIdentity());
break;
}
}
그리고 루트의 델타 Transform을 추출해 MovementComponent에서 Root로 적용시킨다
void CharacterMovementComponent::ConsumeAndApplyRootMotion(float dt)
{
//... 생략
const Transform deltaMeshSpace = anim->ConsumeExtractedRootMotion();
const Transform deltaRootSpace = ConvertMeshSpaceDeltaToRootSpace(deltaMeshSpace, sk);
const Transform deltaScaled = ScaleRootMotionTranslationByRootScale(deltaRootSpace, root);
MoveWithCollision(deltaScaled);
}
