Showing

[Unreal] FRotator와 FRotationMatrix를 통해 캐릭터 이동 본문

Unreal

[Unreal] FRotator와 FRotationMatrix를 통해 캐릭터 이동

RabbitCode 2023. 12. 29. 19:58

언리얼에서 제공하는 FRotator와 FRotationMatrix를 사용하여 플레이어의 Move를 구현할 때의 방법과 함수의 원리를 간단히 기록해두고자 한다.

 

우선 FRotator회전 Matrix R의 관계를 정리하면,

  • FRotator는 회전을 나타내는 구조체이며, Yaw, Pitch, Roll 값으로 회전을 정의한다.
  • 회전 매트릭스는 회전 변환을 나타내는 행렬이며, 회전할 때 곱하는 행렬 R이다.
  • FRotationMatrix 생성자는 FRotator를 기반으로 회전 매트릭스를 생성한다.
  • FRotationMatrix의 MakeFromX, MakeFromY, MakeFromZ 함수들은 주어진 축을 기반으로 회전 매트릭스를 생성하는 데 사용한다. 
  • FRotationMatrix()로 생성된 회전 매트릭스는 GetUnitAxis를 사용하여 매트릭스에서 원하는 축의 단위 벡터를 가져올 수 있다.
// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreTypes.h"
#include "Math/Vector.h"
#include "Math/Rotator.h"
#include "Math/Matrix.h"
#include "Math/RotationTranslationMatrix.h"
#include "Math/QuatRotationTranslationMatrix.h"

namespace UE {
namespace Math {

/** Rotation matrix no translation */
template<typename T>
struct TRotationMatrix
	: public TRotationTranslationMatrix<T>
{
public:
	using TRotationTranslationMatrix<T>::M;
	TRotationMatrix(const TRotator<T>& Rot)
		: TRotationTranslationMatrix<T>(Rot, TVector<T>::ZeroVector)
	{ }

	/** Matrix factory. Return an TMatrix<T> so we don't have type conversion issues in expressions. */
	static TMatrix<T> Make(TRotator<T> const& Rot)
	{
		return TRotationMatrix(Rot);
	}

	/** Matrix factory. Return an TMatrix<T> so we don't have type conversion issues in expressions. */
	static TMatrix<T> Make(TQuat<T> const& Rot);

	/** Builds a rotation matrix given only a XAxis. Y and Z are unspecified but will be orthonormal. XAxis need not be normalized. */
	static TMatrix<T> MakeFromX(TVector<T> const& XAxis);

	/** Builds a rotation matrix given only a YAxis. X and Z are unspecified but will be orthonormal. YAxis need not be normalized. */
	static TMatrix<T> MakeFromY(TVector<T> const& YAxis);

	/** Builds a rotation matrix given only a ZAxis. X and Y are unspecified but will be orthonormal. ZAxis need not be normalized. */
	static TMatrix<T> MakeFromZ(TVector<T> const& ZAxis);

	/** Builds a matrix with given X and Y axes. X will remain fixed, Y may be changed minimally to enforce orthogonality. Z will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromXY(TVector<T> const& XAxis, TVector<T> const& YAxis);

	/** Builds a matrix with given X and Z axes. X will remain fixed, Z may be changed minimally to enforce orthogonality. Y will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromXZ(TVector<T> const& XAxis, TVector<T> const& ZAxis);

	/** Builds a matrix with given Y and X axes. Y will remain fixed, X may be changed minimally to enforce orthogonality. Z will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromYX(TVector<T> const& YAxis, TVector<T> const& XAxis);

	/** Builds a matrix with given Y and Z axes. Y will remain fixed, Z may be changed minimally to enforce orthogonality. X will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromYZ(TVector<T> const& YAxis, TVector<T> const& ZAxis);

	/** Builds a matrix with given Z and X axes. Z will remain fixed, X may be changed minimally to enforce orthogonality. Y will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromZX(TVector<T> const& ZAxis, TVector<T> const& XAxis);

	/** Builds a matrix with given Z and Y axes. Z will remain fixed, Y may be changed minimally to enforce orthogonality. X will be computed. Inputs need not be normalized. */
	static TMatrix<T> MakeFromZY(TVector<T> const& ZAxis, TVector<T> const& YAxis);
};

template<typename T>
TMatrix<T> TRotationMatrix<T>::Make(TQuat<T> const& Rot)
{
	return TQuatRotationTranslationMatrix<T>(Rot, TVector<T>::ZeroVector);
}

template<typename T>
TMatrix<T> TRotationMatrix<T>::MakeFromX(TVector<T> const& XAxis)
{
	TVector<T> const NewX = XAxis.GetSafeNormal();

	// try to use up if possible
	TVector<T> const UpVector = (FMath::Abs(NewX.Z) < (1.f - UE_KINDA_SMALL_NUMBER)) ? TVector<T>(0, 0, 1.f) : TVector<T>(1.f, 0, 0);

	const TVector<T> NewY = (UpVector ^ NewX).GetSafeNormal();
	const TVector<T> NewZ = NewX ^ NewY;

	return TMatrix<T>(NewX, NewY, NewZ, TVector<T>::ZeroVector);
}

template<typename T>
TMatrix<T> TRotationMatrix<T>::MakeFromY(TVector<T> const& YAxis)
{
	TVector<T> const NewY = YAxis.GetSafeNormal();

	// try to use up if possible
	TVector<T> const UpVector = (FMath::Abs(NewY.Z) < (1.f - UE_KINDA_SMALL_NUMBER)) ? TVector<T>(0, 0, 1.f) : TVector<T>(1.f, 0, 0);

	const TVector<T> NewZ = (UpVector ^ NewY).GetSafeNormal();
	const TVector<T> NewX = NewY ^ NewZ;

	return TMatrix<T>(NewX, NewY, NewZ, TVector<T>::ZeroVector);
}

template<typename T>
TMatrix<T> TRotationMatrix<T>::MakeFromZ(TVector<T> const& ZAxis)
{
	TVector<T> const NewZ = ZAxis.GetSafeNormal();

	// try to use up if possible
	TVector<T> const UpVector = (FMath::Abs(NewZ.Z) < (1.f - UE_KINDA_SMALL_NUMBER)) ? TVector<T>(0, 0, 1.f) : TVector<T>(1.f, 0, 0);

	const TVector<T> NewX = (UpVector ^ NewZ).GetSafeNormal();
	const TVector<T> NewY = NewZ ^ NewX;

	return TMatrix<T>(NewX, NewY, NewZ, TVector<T>::ZeroVector);
}

} // namespace Math
} // namespace UE

UE_DECLARE_LWC_TYPE(RotationMatrix, 44);

template<> struct TIsUECoreVariant<FRotationMatrix44f> { enum { Value = true }; };
template<> struct TIsUECoreVariant<FRotationMatrix44d> { enum { Value = true }; };

 

  • MakeFromX 함수는 TVector<T> 형식의 XAxis 벡터를 입력으로 받아서, 이 벡터를 기반으로 새로운 회전 매트릭스를 생성한다. 즉 주어진 XAxis 벡터를 기준으로 회전 매트릭스를 생성하여 해당 벡터를 X 축으로 사용하고, 나머지 Y와 Z 축을 계산한다.
template<typename T>
TMatrix<T> TRotationMatrix<T>::MakeFromX(TVector<T> const& XAxis)
{
	TVector<T> const NewX = XAxis.GetSafeNormal();

	// try to use up if possible
	TVector<T> const UpVector = (FMath::Abs(NewX.Z) < (1.f - UE_KINDA_SMALL_NUMBER)) ? TVector<T>(0, 0, 1.f) : TVector<T>(1.f, 0, 0);

	const TVector<T> NewY = (UpVector ^ NewX).GetSafeNormal();
	const TVector<T> NewZ = NewX ^ NewY;

	return TMatrix<T>(NewX, NewY, NewZ, TVector<T>::ZeroVector);
}

 

첫번째 방법

GetControlRotation를 통해 FRotator( 컨트롤러의 회전 값)를 가져오고, 이를 기반으로 FRotationMatrix생성자를 이용하여 회전매트릭스 R을 만든다.( 컨트롤 로테이션의 FRotator 값으로 회전 행렬을 생성 )

그 매트릭스에서 GetUnitAxis(EAxis::X)과 GetUnitAxis(EAxis::Y)를 호출하여 X축, Y축의 단위 벡터를 얻는다.( 이동 방향 도출 ) 이렇게 얻은 벡터는 회전된 상태에 따라 움직일 방향(ForwardVector와 RightVector)을 나타낸다.

그런 다음 AddMovementInput 함수에 이렇게 만든 방향 벡터 (ForwardVector와 RightVector) 를 전달하여 이동을 제어할 수 있다.

void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
{
	UPawnMovementComponent* MovementComponent = GetMovementComponent();
	if (MovementComponent)
	{
		MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
	}
	else
	{
		Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
	}
}

요약하자면, 컨트롤러의 회전 값을 사용하여 FRotator를 생성하고, 이를 통해 FRotationMatrix를 만들어서 ForwardVector와 RightVector를 추출한 후, 이 벡터 플레이어의 입력만큼 곱하여 캐릭터를 이동시키는 것이다.

void ASViewCharacter::Move(const FInputActionValue& InValue)
{
	FVector2D MovementVector = InValue.Get<FVector2D>(); // MovementVector 입력으로 받은 이동 벡터

	switch (CurrentViewMode)
	{
	case EViewMode::None:
		break;
	case EViewMode::BackView:
	{ // Switch-Case 구문 내에서 Scope를 지정하면 해당 Scope 내에서 변수 선언이 가능해짐.
		const FRotator ControlRotation = GetController()->GetControlRotation();
		const FRotator ControlRotationYaw(0.f, ControlRotation.Yaw, 0.f);

		// FRotator를 통해 매트릭스를 가져온다
		const FVector ForwardVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::X);
		const FVector RightVector = FRotationMatrix(ControlRotationYaw).GetUnitAxis(EAxis::Y);

		AddMovementInput(ForwardVector, MovementVector.X);
		AddMovementInput(RightVector, MovementVector.Y);

		GetCharacterMovement()->bOrientRotationToMovement = true;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		GetCharacterMovement()->RotationRate = FRotator(0.f, 480.f, 0.f);

		break;
	}
	case EViewMode::QuarterView:
    // DirectionToMove에 입력 벡터 저장 후 tick에서 관리
		DirectionToMove.X = MovementVector.X;
		DirectionToMove.Y = MovementVector.Y;

		break;
	case EViewMode::End:
		break;
	default:
		AddMovementInput(GetActorForwardVector(), MovementVector.X);
		AddMovementInput(GetActorRightVector(), MovementVector.Y);
		break;
	}
}

두번째 방법

방향이 있는 입력 벡터( 입력 이벤트 함수에서 X축 값과 Y축 값으로 방향 벡터 값을 생성 )를 통해 x축을 해석하여 만든 매트릭스(  캐릭터의 시선 방향, MakeFromX() 함수 사용하여 직교하는 나머지 두 축을 구해서 회전 행렬 생성 )에서 Rotator을 컨트롤러에 SetControlRotation해주고 입력 벡터 그대로 AddMovementInput함수에 넣어주는 방식도 가능하다.

//입력 로직 -> 게임 로직 -> 애니메이션 로직
void ASViewCharacter::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	switch (CurrentViewMode)
	{
	case EViewMode::BackView:
		break;
	case EViewMode::QuarterView:
	{
		if (KINDA_SMALL_NUMBER < DirectionToMove.SizeSquared()) // SizeSquared : norm : x, y, z 제곱하고 루트씌운 것
		{ // 0.0x0x0x보가 아주 조금이라도 커지게 된다면,
			// MakeFromX : 3차원 상의 점인 벡터값을 x축으로 해석하겠다
			// 매트릭스를 가지고 Rotator를 가지고 온다
   //하나의 벡터로부터 회전 행렬을 구하는 함수 MakeFromX(), MakeFromY(), MakeFromZ() 
   //ex) QuarterView 방식 두 축의 입력을 합산한 벡터와 캐릭터의 시선 방향(X축)이 같아야 하므로 MakeFromX()가 사용됨.
			GetController()->SetControlRotation(FRotationMatrix::MakeFromX(DirectionToMove).Rotator());
			AddMovementInput(DirectionToMove);
			DirectionToMove = FVector::ZeroVector;
		}
		break;
	}
	case EViewMode::End:
		break;
	default:
		break;
	}
}

 

- (참고)캐릭터 무브먼트의 bUseControllerDesiredRotation 속성을 활용하면 컨트롤 회전이 가리키는 방향으로 캐릭터가 부드럽게 회전

		bUseControllerRotationPitch = false;
		bUseControllerRotationYaw = false;
		bUseControllerRotationRoll = false;

		GetCharacterMovement()->bOrientRotationToMovement = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		GetCharacterMovement()->RotationRate = FRotator(0.f, 480.f, 0.f);

FRotationMatrix를 쓰지 않는 세번째 방법

void AMyPlayer::Move(const FInputActionValue& Value)
{
	const FVector _currentValue = Value.Get<FVector>();
	if (Controller) {
		moveDirection.Y = _currentValue.X;
		moveDirection.X = _currentValue.Y;
	}
}

 

void AMyPlayer::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 이동방향을 컨트롤 방향 기준으로 변환
	moveDirection = FTransform(GetControlRotation()).TransformVector(moveDirection);
	
    AddMovementInput(moveDirection);
	// 방향 초기화
	// 방향이 누적되지 않게
	moveDirection = FVector::ZeroVector;
}

 

FTransform(GetControlRotation()).TransformVector(moveDirection)

현재 컨트롤러의 회전을 기반으로 moveDirection 벡터를 변환한다.

  1. GetControlRotation(): 이 함수는 Unreal Engine에서 현재 컨트롤러(일반적으로 플레이어 캐릭터의 컨트롤러)의 회전 정보 FRotator를 가져온다.
  2. FTransform(GetControlRotation()): 여기서 GetControlRotation()으로 얻은 회전 정보를 FTransform 형태로 변환한다. FTransform은 3D 공간에서 변환을 나타내는 구조체로, 위치와 회전을 포함하고 있다. 따라서 컨트롤러의 회전을 나타내는 FRotator를 FTransform으로 변환하면 이동 없이 회전만 있는 변환 매트릭스가 된다.
  3. .TransformVector(moveDirection): moveDirection 벡터를 앞에서 생성한 변환 매트릭스를 사용하여 회전시킨다. 즉, moveDirection 벡터를 현재 컨트롤러의 방향으로 회전시킨다.
  4. AddMovementInput(moveDirection);: 이 부분은 회전된 moveDirection 벡터를 사용하여 이동 입력을 추가한다. 캐릭터나 오브젝트는 AddMovementInput 함수를 호출함으로써 움직일 방향과 속도를 지정할 수 있다. 따라서 이 코드는 moveDirection 벡터에 따라 캐릭터나 오브젝트를 움직이게 된다.
  5. moveDirection = FVector::ZeroVector;: 이 부분은 moveDirection 벡터를 초기화한다. 이것은 이전에 추가된 이동 입력이 누적되지 않도록 하는 역할을 한다. 예를 들어, 한 프레임에서 moveDirection을 (1, 0, 0)으로 설정하고 움직임을 추가하면, 다음 프레임에서도 moveDirection이 (1, 0, 0)으로 유지되지 않고 초기화된다. 따라서 이전의 움직임이 새로운 움직임에 영향을 미치지 않는다.

결과적으로, moveDirection 벡터는 현재 컨트롤러의 방향으로 회전되며, 이 회전된 벡터를 이동 입력으로 사용하여 캐릭터나 오브젝트를 이동킨다. 이를 통해 컨트롤러의 방향에 따라 움직임을 구현할 수 있다.