모델의 관절이나 중요점들에 뼈대를 만들어 두고 텍스쳐가 뼈대의 움직임을 따라 움직이도록 함
이 때 뼈대는 트리 구조를 가지고 상위 노드가 움직일 시 하위 노드들이 따라 움직이게 됨
기본적으로 모델은 전체 모델의 로컬 좌표계 기준으로 되어 있고
이를 뼈대 기준으로 변환을 한번 시켜야 함 (offset transform)
단 뼈대와 스킨 버텍스의 관계가 1:1이 아니라 n:1이 될 수 있음
=>
정점이 뼈대 가중치에 따라 다르게 영향 받음
프레임 마다 뼈대를 월드 좌표로 바꾸는 행렬들을 파일로 만들어 두고
이를 불러와서 뼈대를 이동 시킴
=>
애니메이션
이 때 행렬들을 키프레임이라 하고 이를 시간에 따라 블렌딩하여 자연스럽게 보이도록 함
mesh 클래스 수정
struct KeyFrameInfo
{
double time;
int32 frame;
Vec3 scale;
Vec4 rotation;
Vec3 translate;
};
struct BoneInfo
{
wstring boneName;
int32 parentIdx;
Matrix matOffset;
};
struct AnimClipInfo
{
wstring animName;
int32 frameCount;
double duration;
vector<vector<KeyFrameInfo>> keyFrames;
};
키프레임 정보는 시작 시간과 프레임 번호를 가지며,
SRT 정보를 각각 가지고 있음 => 블렌딩을 쉽게 하기 위해
class Mesh : public Object
{
...
private:
...
void CreateBonesAndAnimations(class FBXLoader& loader);
Matrix GetMatrix(FbxAMatrix& matrix);
...
private:
...
vector<AnimClipInfo> _animClips;
vector<BoneInfo> _bones;
shared_ptr<StructuredBuffer> _offsetBuffer;
vector<shared_ptr<StructuredBuffer>> _frameBuffer;
};
메쉬에서는 뼈대와 스킨, 애니메이션들을 FBX 파일로부터 받아 저장
스키닝은 CPU가 할 수 있지만 GPU에서 하는 것이 더 빠름 (하드웨어 스키닝)
animator 클래스 추가
class Animator : public Component
{
public:
...
void SetBones(const vector<BoneInfo>* bones) { _bones = bones; }
void SetAnimClip(const vector<AnimClipInfo>* animClips);
void PushData();
int32 GetAnimCount() { return static_cast<uint32>(_animClips->size()); }
int32 GetCurrentClipIndex() { return _clipIndex; }
void Play(uint32 idx);
public:
virtual void FinalUpdate() override;
private:
const vector<BoneInfo>* _bones;
const vector<AnimClipInfo>* _animClips;
...
};
메쉬를 가지고 애니메이션을 재생시키는 클래스 구현
void Animator::FinalUpdate()
{
...
const int32 ratio = static_cast<int32>(animClip.frameCount / animClip.duration);
_frame = static_cast<int32>(_updateTime * ratio);
_frame = min(_frame, animClip.frameCount - 1);
_nextFrame = min(_frame + 1, animClip.frameCount - 1);
_frameRatio = static_cast<float>(_frame - _frame);
}
애니메이션 블렌딩을 위해 final update 시에 현재 프레임과 다음 프레임 간의 비율을 구함
void Animator::PushData()
{
...
shared_ptr<Mesh> mesh = GetGameObject()->GetMeshRenderer()->GetMesh();
mesh->GetBoneFrameDataBuffer(_clipIndex)->PushComputeSRVData(SRV_REGISTER::t8);
mesh->GetBoneOffsetBuffer()->PushComputeSRVData(SRV_REGISTER::t9);
_boneFinalMatrix->PushComputeUAVData(UAV_REGISTER::u0);
...
_computeMaterial->Dispatch(groupCount, 1, 1);
_boneFinalMatrix->PushGraphicsData(SRV_REGISTER::t7);
}
하드웨어 스키닝을 위하여 컴퓨트 쉐이더를 이용
shader 클래스 수정
void Shader::CreateGraphicsShader(const wstring& path, ShaderInfo info, ShaderArg arg)
{
...
D3D12_INPUT_ELEMENT_DESC desc[] =
{
...
{ "WEIGHT", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 44, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "INDICES", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 60, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
...
};
...
}
컴퓨트 쉐이더에서 만든 정보를 사용하기 위해
쉐이더 디스크립터에 스키닝을 위한 weight와 indices를 추가
mesh renderer 클래스 수정
void MeshRenderer::Render(shared_ptr<InstancingBuffer>& buffer)
{
for (uint32 i = 0; i < _materials.size(); i++)
{
...
if (GetAnimator())
{
GetAnimator()->PushData();
material->SetInt(1, 1);
}
...
}
}
컴포넌트에 애니메이터 컴포넌트가 있을 시 함께 데이터를 넘겨줌
animation 쉐이더 파일
struct AnimFrameParams
{
float4 scale;
float4 rotation;
float4 translation;
};
[numthreads(256, 1, 1)]
void CS_Main(int3 threadIdx : SV_DispatchThreadID)
{
...
float4 scale = lerp(g_bone_frame[idx].scale, g_bone_frame[nextIdx].scale, ratio);
float4 rotation = QuaternionSlerp(g_bone_frame[idx].rotation, g_bone_frame[nextIdx].rotation, ratio);
float4 translation = lerp(g_bone_frame[idx].translation, g_bone_frame[nextIdx].translation, ratio);
matrix matBone = MatrixAffineTransformation(scale, quaternionZero, rotation, translation);
g_final[threadIdx.x] = mul(g_offset[threadIdx.x], matBone);
}
애니메이션에 사용하기 위한 변환 행렬을 만드는 컴퓨트 쉐이더에서는
현재 프레임과 다음 프레임 정보를 가지고 보간하여 만듬
deferred 쉐이더 파일 수정
VS_OUT VS_Main(VS_IN input)
{
...
if (g_int_0 == 1)
{
if (g_int_1 == 1)
Skinning(input.pos, input.normal, input.tangent, input.weight, input.indices);
...
}
else
{
if (g_int_1 == 1)
Skinning(input.pos, input.normal, input.tangent, input.weight, input.indices);
...
}
return output;
}
버텍스 쉐이더에서 전역 변수를 통하여 스키닝 정보를 사용하도록 수정
struct SkinningInfo
{
float3 pos;
float3 normal;
float3 tangent;
};
void Skinning(inout float3 pos, inout float3 normal, inout float3 tangent,
inout float4 weight, inout float4 indices)
{
...
for (int i = 0; i < 4; ++i)
{
...
matrix matBone = g_mat_bone[boneIdx];
info.pos += (mul(float4(pos, 1.f), matBone) * weight[i]).xyz;
info.normal += (mul(float4(normal, 0.f), matBone) * weight[i]).xyz;
info.tangent += (mul(float4(tangent, 0.f), matBone) * weight[i]).xyz;
}
pos = info.pos;
tangent = normalize(info.tangent);
normal = normalize(info.normal);
}
스키닝 시에 뼈대 가중치에 따라 위치 및 노말, 탄젠트 벡터들을 수정