Animation(skinning)

ㅋㅋ·2022년 7월 31일
0

DirectX12강의

목록 보기
39/39

모델의 관절이나 중요점들에 뼈대를 만들어 두고 텍스쳐가 뼈대의 움직임을 따라 움직이도록 함

이 때 뼈대는 트리 구조를 가지고 상위 노드가 움직일 시 하위 노드들이 따라 움직이게 됨

기본적으로 모델은 전체 모델의 로컬 좌표계 기준으로 되어 있고

이를 뼈대 기준으로 변환을 한번 시켜야 함 (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);
}

스키닝 시에 뼈대 가중치에 따라 위치 및 노말, 탄젠트 벡터들을 수정

0개의 댓글