본문 바로가기

그래픽스

[DX12] Animation

2D에서 애니메이션은 프레임마다 그림을 새로 그려 재생했을 때 움직이는 것처럼 보이게 만든다. 하지만 3D에서도 그런 방식을 채용하기에는 무리가 있다. 왜냐하면 수많은 정점들을 가지고 있는 Mesh들이 있는데 그것의 정점들을 하나하나 다 바꿔야 한다면 말도 안되게 데이터가 늘어날 것이다. 그래서 Skinning이라는 것을 이용한다.

 

okino.com

 

Skinning은 뼈대를 만들어 피부(정점)가 뼈에 붙어 뼈와 같이 움직인다. 뼈들은 계층 구조를 이루고 부모 뼈가 움직이면 자식 뼈도 움직인다. 즉 자식 뼈 기준에서 좌표가 변하지 않아도 부모 뼈 기준에서 좌표가 변할 수 있으므로 뼈마다 자신의 좌표계를 가지고 있다고 볼 수 있다. 

 

 

자식뼈의 local 좌표계에서 부모뼈의 local좌표계로 올라가다보면 가장 최상위 부모의 local 좌표계에서 올라가면 world 좌표로 바꿀 수 있다. 그래서 자식 뼈에서 부모 뼈 좌표로 가는 행렬을 A라고 할 때 그림과 같은 식으로 표현할 수 있다. 그리고 재귀적으로도 표현할 수 있다. 그래서 초기 바인드 포지션 기준 좌표를 로컬 본 기준으로 변환해야 하는데 이것을 오프셋 변환이라고 한다. 최상위 뼈까지 도착하면 원래 사용하던 Local -> World로 변환하면 된다.

 

아까 피부(정점)가 뼈에 붙어있다고 했는데 이들이 일대일 대응관계는 아니다. 예를 들어 어깨를 움직이면 가슴 쪽 근육도 살짝 움직일 것이므로 한 정점이 여러 뼈의 영향을 받을 수 있다. 그래서 가중치를 두어 계산한다. 그래서 영향을 받는 정점 별로 비율을 설정해 어떻게 움직이는지 정해준다. 최종적으로는 Offset 행렬과 ToRoot 행렬을 곱한 Final 행렬을 구한다. 

그래서 뼈마다 , 프레임마다 Final 행렬을 계산한다.

 

void FBXLoader::LoadBones(FbxNode* node, int32 idx, int32 parentIdx)
{
	FbxNodeAttribute* attribute = node->GetNodeAttribute();

	if (attribute && attribute->GetAttributeType() == FbxNodeAttribute::eSkeleton)
	{
		shared_ptr<FbxBoneInfo> bone = make_shared<FbxBoneInfo>();
		bone->boneName = s2ws(node->GetName());
		bone->parentIndex = parentIdx;
		_bones.push_back(bone);
	}

	const int32 childCount = node->GetChildCount();
	for (int32 i = 0; i < childCount; i++)
		LoadBones(node->GetChild(i), static_cast<int32>(_bones.size()), idx);
}

 

코드에서는 먼저 FBXLoader에서 Mesh를 불러올 때 LoadBones로 Root Bone부터 자식을 찾아가며 모든 Bone을 추가한다. 그리고 LoadAnimationInfo로 애니메이션에 대한 기본적인 정보를 불러온다.

 

struct BoneWeight
{
	using Pair = pair<int32, double>;
	vector<Pair> boneWeights;
    ...
struct FbxAnimClipInfo
{
	wstring			name;
	FbxTime			startTime;
	FbxTime			endTime;
	FbxTime::EMode	mode;
	vector<vector<FbxKeyFrameInfo>>	keyFrames;
};

 

다음은 본격적인 애니메이션 정보를 Load 하기 위해 LoadAnimationData를 실행한다. 여기서는 LoadBoneWeight로 아까 말한 가중치를 저장한다. BoneWeight는 pair을 가진 구조체로 pair은 이 뼈에 영향을 주는 정점들과 가중치 비율을 가지고 있다. 그리고 AnimClipInfo에서는 startTime, endTime, keyFrames를 가지고 있는데 이것도 아까 말한 뼈별로 프레임마다 

가지고 있는 정보를 2차원 벡터로 가지고 있다. LoadKeyFrames에서 키프레임들을 채운다. 

 

shared_ptr<StructuredBuffer>	_offsetBuffer; // 각 뼈의 offset 행렬
vector<shared_ptr<StructuredBuffer>> _frameBuffer; // 전체 본 프레임 정보

 

프레임 계산을 GPU에서 해서 빠르게 처리하기 위해  StructuredBuffer로 만들어준다. 

 

//animation.fx
StructuredBuffer<AnimFrameParams>   g_bone_frame : register(t8);
StructuredBuffer<matrix>            g_offset : register(t9);
RWStructuredBuffer<matrix>          g_final : register(u0);

// ComputeAnimation
// g_int_0 : BoneCount
// g_int_1 : CurrentFrame
// g_int_2 : NextFrame
// g_float_0 : Ratio
[numthreads(256, 1, 1)]
void CS_Main(int3 threadIdx : SV_DispatchThreadID)
{
    if (g_int_0 <= threadIdx.x)
        return;

    int boneCount = g_int_0;
    int currentFrame = g_int_1;
    int nextFrame = g_int_2;
    float ratio = g_float_0;

    uint idx = (boneCount * currentFrame) + threadIdx.x;
    uint nextIdx = (boneCount * nextFrame) + threadIdx.x;

    float4 quaternionZero = float4(0.f, 0.f, 0.f, 1.f);

    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);
}

 

animation.fx에서는 frame에서 정보를 받아와서 프레임과 다음 프레임 사이를 보간한다. CS에서 g_bone_frame과 g_offset을 이용해 보간한 값을 최종적으로 g_final에 저장한다. 

 

void Animator::PushData()
{
	uint32 boneCount = static_cast<uint32>(_bones->size());
	if (_boneFinalMatrix->GetElementCount() < boneCount)
		_boneFinalMatrix->Init(sizeof(Matrix), boneCount);

	// Compute Shader
	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->SetInt(0, boneCount);
	_computeMaterial->SetInt(1, _frame);
	_computeMaterial->SetInt(2, _nextFrame);
	_computeMaterial->SetFloat(0, _frameRatio);

	uint32 groupCount = (boneCount / 256) + 1;
	_computeMaterial->Dispatch(groupCount, 1, 1);

	// Graphics Shader
	_boneFinalMatrix->PushGraphicsData(SRV_REGISTER::t7);
}

 

Animator::PushData에서 u0레지스터에 Compute Shader로 계산한 값을 넣어주고  Dispatch로 계산을 해서 t7 레지스터에

deferrend Rendering 계산을 완료한 결과값이 저장된다.

 

  if (g_int_0 == 1)
    {
        if (g_int_1 == 1)
            Skinning(input.pos, input.normal, input.tangent, input.weight, input.indices);

        output.pos = mul(float4(input.pos, 1.f), input.matWVP);
        output.uv = input.uv;

        output.viewPos = mul(float4(input.pos, 1.f), input.matWV).xyz;
        output.viewNormal = normalize(mul(float4(input.normal, 0.f), input.matWV).xyz);
        output.viewTangent = normalize(mul(float4(input.tangent, 0.f), input.matWV).xyz);
        output.viewBinormal = normalize(cross(output.viewTangent, output.viewNormal));
    }
void Skinning(inout float3 pos, inout float3 normal, inout float3 tangent,
    inout float4 weight, inout float4 indices)
{
    SkinningInfo info = (SkinningInfo)0.f;

    for (int i = 0; i < 4; ++i)
    {
        if (weight[i] == 0.f)
            continue;

        int boneIdx = indices[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);
StructuredBuffer<Matrix> g_mat_bone : register(t7);

 

계산한 것들을 deferred rendering 하는데 g_int_1이 켜져있을 경우 Skinning으로 인식한다. weight를 보고 가중치에 따라 pos, normal, tangent를 세팅한다. 그래서 결과 값이 Root의 local 좌표계이므로 matWVP를 곱해 기존과 같이 계산한다.

g_matBone에 저장되는데 params.fx에 새로 추가된 StructuredBuffer이다.

'그래픽스' 카테고리의 다른 글

[그래픽스] 선과 구의 충돌  (0) 2023.05.11
[그래픽스] Kernel, Convolution, Gaussian Blur  (0) 2023.05.10
[DX12] Picking  (0) 2022.07.31
[DX12] Terrain  (0) 2022.07.31
[DX12] Tessellation  (0) 2022.07.31