[Unity] IK를 이용한 자연스러운 경사 움직임
1인칭 플레이어 움직임을 만들 때 근본적인 문제점이 발생했다.
플레이어가 어떻게 자연스럽게 경사를 올라가게 할 것인가?
일반적인 애니메이션과 콜라이더를 사용하여 경사를 올라가면, 발이 다른 오브젝트의 매쉬를 뚫거나 발이 공중에 떠있게 될 것이다
이를 해결하려면, 발에서 Ray를 쏴서 바닥과 발 사이의 각도를 구한 다음 발을 회전시켜야 할까?라고 생각하고 검색을 해보던 도중에, IK 기능을 발견했다.
유튜브에 올라와 있는 강의를 참고해서 작성하였다
https://www.youtube.com/watch?v=rGB1ipH6DrM&t=133s
📝IK?
일단. IK가 무엇인지 알아야 한다. 유니티 공식문서에 따르면 , 대부분의 애니메이션은 스켈레톤의 조인트 각도를 미리 정해진 값으로 회전하여 만든다. 자식 뼈대 (조인트)는 부모 뼈대 (조인트)의 회전에 따라 변하고, 이런 방식을 순 운동학 (FK)라고 한다.
하지만 이런 방법을 반대로 바라본다면 더욱 유용하다. 오브젝트를 기준으로 뼈대 (조인트) 값을 계산하여 적용하는 것이다. 예를 들어 플레이어가 다른 오브젝트를 건드리거나, 울퉁불퉁한 표면 위에 캐릭터의 두 발이 자연스럽게 밀착해 있도록 할 수 있다. 이러한 접근법을 역운동학 (IK)라고 한다.
IK를 적용하기 위해서는 아바타의 타입이 휴머노이드여야 한다.
📝 사전준비
1. 플레이어의 Animator의 Layer의 설정에서 IK Pass 옵션을 체크해준다
2. 플레이어가 걸어갈 발판/경사로 등의 레이어를 Walkable로 지정해 준다
📝구현
PlayerAnimator.cs
public class PlayerAnimator : MonoBehaviour
{
[Header("===Animator===")]
[SerializeField] private Animator animator;
[Range(0, 1f)]
public float distanceToGround;
public LayerMask layerMask;
void Start()
{
animator = GetComponent<Animator>();
}
private void OnAnimatorIK(int layerIndex)
{
}
}
□ void OnAnimatorIK(int layerIndex)
■ 애니메이션 IK(역운동학)을 설정하기 위한 콜백
■ Animator Component가 내부 IK 시스템을 업데이트하기 직전에 호출
■ 콜백 안에 IK 목표 위치와 , 가중치를 설정하는 데 사용한다
private void OnAnimatorIK(int layerIndex)
{
// 애니메이터가 실행되는 매 프레임 실행
if(animator != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1f);
□ animator.SetIKPositionWeight()
■ 공식문서에서는, IK 목표의 변환 가중치를 설정한다. ( 0 = IK전 원래 애니메이션에서 1 = 목표에서 )
■ 즉 가중치를 설정하여 원래 애니메이션과 , IK 조정 사이의 혼합정도를 결정한다.
□ animator.SetIKRotationWeight()
■ SetIKPositionWeight과 같이 회전에 대한 가중치를 설정한다.
🔖 가중치
가중치가 0일 때 : 원래 애니메이션이 100% 적용된다. IK는 전혀 영향을 주지 않는다
가중치가 1일 때 : 조정이 100% 적용된다. 원래 애니메이션은 무시된다
가중치가 0 ~ 1 사이일 때 : 원래 애니메이션과 IK 조정값이 해당 비율로 혼합된다
🔖 여기서 AvaterIkGoal은 Enum으로,
LeftFoot과 RightFoot은 Animator에 적용된 아바타의
Left Leg의 Foot과 , Right Leg의 Foot를 의미한다.
// left foot
RaycastHit hit;
Ray ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up, Vector3.down);
Debug.DrawRay(ray.origin, ray.direction * (distanceToGround + 1f), Color.red);
□ Ray 생성
■ 시작지점 : 아바타의 LeftFoot의 위치에서 1f 올라간 곳,
■ 방향 : 시작지점에서 아래쪽 방향
□ DrawRay로 레이를 그리면, 주황색 동그라미에서 시작하여 아래쪽으로 ray를 쏘고 있는 것을 확인할 수 있다.
if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer))
{
if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
{
// 추가예정
// 플레이어 발 위치 , 회전 적용
}
}
□ Raycast
■ 플레이어를 제외한 오브젝트를 감지해야 하기 때문에 PlayerLayer을 제외한 레이어만 검사
■ 레이캐스트가 성공적으로 충돌체를 감지하면 , if문 실행
□ 해당 충돌체의 레이어가 'Walkable'이면?
■ 플레이어 발의 위치와 회전을 변경
(+) 레이어에 관해서
layer을 검사할 땐 layerMask의 인덱스 값으로 검사해야 하기 때문에 GetMask를 사용해 준다
해당코드는 어디에 있던지 상관없다
[SerializeField] private LayerMask ignorePlayerLayer; // 플레이어 레이어를 제외한 레이어
[SerializeField] private int walkableLayerInt;
void Start()
{
walkableLayerInt = LayerMask.NameToLayer("Walkable");
}
PlayerLayer을 뺀 LayerMask 정보는 인스펙터 창에서 PlayerLayer을 제외한 다른 레이어들을 체크해주면 된다.
if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer))
{
if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
{
Vector3 footPosition = hit.point;
footPosition.y += distanceToGround;
animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPosition);
animator.SetIKRotation(AvatarIKGoal.LeftFoot, Quaternion.LookRotation(transform.forward, hit.normal));
}
}
□ 해당 충돌체의 레이어가 'Walkable'이면?
■ 발의 위치
□ hit.point(레이가 충돌한 정확한 지점)를 footPosition으로 사용
■ footPosition.y += distanceToGround
□ 발이 지면에 완전히 묻히지 않고 자연스럽게 위치하도록 약간의 오프셋을 주기 위함
□ 현재 distanceToGround는 0.06f 정도로 설정되어있다
■ 발의 회전
□ Quaternion.LookRotation을 사용해서 플레이어가 바라보고 있는 방향으로,
□ hit의 법선 벡터와 일치하도록 hit.normal을 사용해서 회전값을 줌
전체코드
public class PlayerAnimator : MonoBehaviour
{
[Header("===Animator===")]
[SerializeField] private Animator animator;
[Range(0, 1f)]
public float distanceToGround; // 0.06f
void Start()
{
animator = GetComponent<Animator>();
}
#region Foot Ik
private void OnAnimatorIK(int layerIndex)
{
// 애니메이터가 실행되는 매 프레임 실행
if(animator != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1f);
animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1f);
// left foot
RaycastHit hit;
Ray ray = new Ray(animator.GetIKPosition(AvatarIKGoal.LeftFoot) + Vector3.up / 2, Vector3.down);
Debug.DrawRay(ray.origin, ray.direction * (distanceToGround + 1f), Color.red);
if (Physics.Raycast(ray, out hit, distanceToGround + 1f , LayerManager.Instance.IgnorePlayerLayer))
{
if(hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
{
Vector3 footPosition = hit.point;
footPosition.y += distanceToGround;
animator.SetIKPosition(AvatarIKGoal.LeftFoot, footPosition);
animator.SetIKRotation(AvatarIKGoal.LeftFoot, Quaternion.LookRotation(transform.forward, hit.normal));
}
}
// 오른발
ray = new Ray(animator.GetIKPosition(AvatarIKGoal.RightFoot) + Vector3.up, Vector3.down);
if (Physics.Raycast(ray, out hit, distanceToGround + 1f, LayerManager.Instance.IgnorePlayerLayer))
{
if (hit.transform.gameObject.layer == LayerManager.Instance.WalkableLayerInt)
{
Vector3 footPosition = hit.point;
footPosition.y += distanceToGround;
animator.SetIKPosition(AvatarIKGoal.RightFoot, footPosition);
animator.SetIKRotation(AvatarIKGoal.RightFoot, Quaternion.LookRotation(transform.forward, hit.normal));
}
}
}
}
#endregion
📝 결과
IK를 적용하기 전 모습
IK를 적용한 후
OnAnimatorIK
https://docs.unity3d.com/kr/530/ScriptReference/MonoBehaviour.OnAnimatorIK.html
MonoBehaviour-OnAnimatorIK(int) - Unity 스크립팅 API
Callback for setting up animation IK (inverse kinematics).
docs.unity3d.com
SetIKPositionWeight
https://docs.unity3d.com/kr/530/ScriptReference/Animator.SetIKPositionWeight.html
Animator-SetIKPositionWeight - Unity 스크립팅 API
Sets the translative weight of an IK goal (0 = at the original animation before IK, 1 = at the goal).
docs.unity3d.com
역운동학 (IK)
https://docs.unity3d.com/kr/530/Manual/InverseKinematics.html
역운동학(IK) - Unity 매뉴얼
대부분의 애니메이션은 스켈레톤의 조인트 각도를 미리 정해진 값으로 회전하여 만듭니다. 자식 조인트의 포지션은 부모의 회전에 따라 변하므로 조인트 체인의 끝 점은 체인에 포함된 각 조인
docs.unity3d.com
깃허브
https://github.com/kimYouChae/Sparta_OnlyUp