Make Unreal REAL.
article thumbnail
이득우의 게임 수학

 

게임에서 캐릭터 애니메이션은 가상의 뼈대인 본(Bone)을 캐릭터 메시에 심은 후, 해당 본의 움직임에 맞춰 캐릭터 메시가 변형되는 스켈레탈 애니메이션(Skeletal Animation) 방식을 사용한다.

 

 

스켈레탈 애니메이션을 구현하려면 기존의 메시 체계를 확장해 본을 추가해야 한다.

 

본은 단순히 이동만 하는 것이 아니고 회전하거나 크기도 변경되므로, 본의 정보는 트랜스폼에서 관리한다.

 

 

게임 오브젝트뿐만 아니라 본에서도 트랜스폼이 필요하고, 책의 예제인 CK소프트렌더러에서 제공하는 트랜스폼 구조체는 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에 정의된 트랜스폼 구조체
struct Transform
{
public:
    Transform() = default;
    Transform(const Vector3& InPosition) : Position(InPosition) { }
    Transform(const Vector3& InPosition, const Quaternion& InRotation) : Position(InPosition), Rotation(InRotation) { }
    Transform(const Vector3& InPosition, const Quaternion& InRotation, const Vector3& InScale) : Position(InPosition), Rotation(InRotation), Scale(InScale) { }
    Transform(const Matrix4x4& InMatrix);

    ...

private: // 트랜스폼에 관련된 변수
    Vector3 Position;
    Quaternion Rotation;
    Vector3 Scale = Vector3::One;
};

 

본을 구성하려면 3가지 정보가 필요하다.

  • 메시에 본이 심어질  때 최초의 배치 정보인 바인드포즈(Bindpose)
    향후 본이 움직일 때 상대적인 변화량을 파악하는 기준이 된다.
  • 애니메이션 재생 시 트랜스폼을 저장할 공간
  • 고유한 이름

 

// 책의 예제인 CK소프트렌더러에 정의된 본
class Bone
{
    ...

private:
    size_t _Hash = 0;

    // 본의 이름
    string _Name;

    // 본의 현재 트랜스폼 정보
    Transform _Transform;

    // 본의 최초 트랜스폼 정보
    Transform _BindPose;
};

 

메시는 본이 없는 메시와 있는 메시 두 종류로 나뉜다.

 

배경 오브젝트 등 애니메이션 없이 없는 고정된 메시는 본 정보가 필요하지 않지만, 애니메이션이 필요한 캐릭터 등에는 메시 정보가 있어야 한다.

  • 본이 심어진 메시를 스킨드 메시(Skinned mesh)라고 한다.

 

// 책의 예제인 CK소프트렌더러에 메시 유형을 구분하기 위해 선언된 열거형
enum class MeshType :UINT32
{
    Normal = 0,
    SKinned
};

 

 

메시에 본 정보를 추가한 후에는 본의 움직임에 따라 정점이 함께 움직이도록 본과 정점을 연결해야 하는데, 이 작업을 리깅(Rigging)이라고 한다.

  • 비유하자면 뼈에 살을 붙이는 작업이라고 할 수 있다.

 

 

리깅을 데이터 관점에서 살펴보면, 하나의 정점에 대해 어떤 본들이 영향을 미치는지를 기록하는 것이다.

 

정점에 영향을 미치는 본이 많을수록 메시는 세심하게 변화될 것이지만, 정점의 최종 위치를 구하는 계산량도 늘게 되어 성능과 품질 사이에서 적절히 Trade-off를 하는 것이 중요하다.

 

하나의 정점이 어떤 본들로부터 얼마 만큼의 영향을 받는지의 정보를 가중치(Weight)라고 한다.

  • 영향력의 총합은 편의를 위해 1이 되도록 하는 것이 일반적이다.

 

// 책의 예제인 CK소프트렌더러에 정의된 가중치
struct Weight
{
    vector<string> Bones;
    vector<float>  Values;
};

 

학습을 위해 가장 단순한 형태로 정점당 하나의 본이 연결되어 있도록 설정하면 다음과 같다.

 

 

위와 같은 정사각형의 스킨드 메시 데이터를 코드로 구축하면 아래와 같다.

 

// 책의 예제인 CK소프트렌더러에서 정사각형 스킨드 메시에 수동으로 리깅하는 부분
bool GameEngine::LoadResources()
{
    // 사각 메시
    ...

    // 스킨드 메시 설정
    quadMesh.SetMeshType(MeshType::Skinned);

    // 리깅 수행
    auto& bones = quadMesh.GetBones();
    auto& connectedBones = quadMesh.GetConnectedBones();
    auto& weights = quadMesh.GetWeights();

    bones = {
        {"left", Bone("left", Transform(Vector3(-1.f, 0.f, 0.f) * halfSize))},
        {"right", Bone("right", Transform(Vector3(1.f, 0.f, 0.f) * halfSize))}
    };
    connectedBones = { 1, 1, 1, 1 };
    weights = {
        { {"left"},  {1.f} },
        { {"left"},  {1.f} },
        { {"right"}, {1.f} },
        { {"right"}, {1.f} }
    };

    ...
}

 

스킨드 메시 설정이 완료되면, 이제부터는 스키닝 애니메이션의 진행을 위해 시간에 따라 본을 움직여줘야 한다.

 

본의 움직임을 구현하기 위해 게임 엔진은 시간에 따라 값의 변화를 저장한 키프레임(Keyframe) 데이터를 사용한다.

  • 점이 찍힌 주요 지점에서 값을 지정하면, 나머지 중간 값은 수식을 통해 보간하여 끊김 없는 데이터를 제공하도록 구성되어 있다.

 

 

캐릭터 애니메이션은 게임 오브젝트의 모든 상태가 최종 결정된 게임 로직이 완료된 시점에서 진행되어야 한다.

 

이를 위해 책의 예제인 CK소프트렌더러에서는 LastUpdate3D()라는 함수를 별도로 선언해, 게임 로직이 구현되는 Update3D() 함수 이후에 호출되도록 구현되어 있다.

 

 

LastUpdate3D() 함수에서 sin 함수를 사용해 스킨드 메시에 설정된 Left 본과 Right 본을 좌우로 왕복시키는 코드는 다음과 같다.

 

// 책의 예제인 CK소프트렌더러에서 sin 함수로 Left, Right 본을 왕복시키는 코드
Vector3 leftBonePosition;
Vector3 rightBonePosition;

// 애니메이션 로직을 담당하는 함수
void SoftRenderer::LateUpdate3D(float InDeltaSeconds)
{
	// 애니메이션 로직에서 사용하는 모듈 내 주요 레퍼런스
	GameEngine& g = Get3DGameEngine();

	// 애니메이션 로직의 로컬 변수
	static float duration = 3.f;
	static float elapsedTime = 0.f;

	// 애니메이션을 위한 커브 생성 ( 0~1 SineWave )
	elapsedTime = Math::Clamp(elapsedTime + InDeltaSeconds, 0.f, duration);
	if (elapsedTime == duration)
	{
		elapsedTime = 0.f;
	}
	float sinParam = elapsedTime * Math::TwoPI / duration;
	float sinWave = (sinf(sinParam) + 1.f) * 0.5f;

	GameObject& goPlayer = g.GetGameObject(PlayerGo);
	Mesh& m = g.GetMesh(goPlayer.GetMeshKey());
	if (!m.IsSkinnedMesh())
	{
		return;
	}

	const string leftBoneName("left");
	const string rightBoneName("right");
	Transform& leftBoneTransform = m.GetBone(leftBoneName).GetTransform();
	Transform& rightBoneTransform = m.GetBone(rightBoneName).GetTransform();

	Vector3 deltaLeftPosition = Vector3::UnitX * -sinWave;
	Vector3 deltaRightPosition = Vector3::UnitX * sinWave;

	leftBonePosition = m.GetBindPose(leftBoneName).GetPosition() + deltaLeftPosition;
	rightBonePosition = m.GetBindPose(rightBoneName).GetPosition() + deltaRightPosition;

	leftBoneTransform.SetPosition(leftBonePosition);
	rightBoneTransform.SetPosition(rightBonePosition);
}

 

'게임 수학 > 이득우의 게임 수학' 카테고리의 다른 글

캐릭터 메시와 애니메이션  (0) 2023.06.11
트랜스폼 계층 구조  (0) 2023.06.10
사원수의 보간  (0) 2023.06.08
사원수의 변환  (0) 2023.06.07
사원수의 회전  (0) 2023.06.06
profile

Make Unreal REAL.

@diesuki4

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그