모름

관련링크

1. FABRIK 알고리즘 : http://sean.cm/a/fabrik-algorithm-2d

2. 관련 튜토리얼 : https://youtu.be/qqOAzn05fvk

 

소개

이 글은 위 링크의 내용을 토대로 작성됐습니다. IK(역운 동학)를 유니티에 구현하는 과정을 정리한 글입니다. 저도 공부를 하면서 정리를 하고 있기 때문에 내용이 정확하지 않을 수 있으니 참고해주세요.

이번 글에선 (1)역운동학, (2)FABRIK 알고리즘, (3) 간단한 유니티 실습 이렇게 총 세 가지 내용을 알아보겠습니다.

 

(1)역운동학(IK)이란?

역운동학이란 '어떤 데이터를 통해 다른 오브젝트의 움직임을 회복하는 수학적 과정(위키피디아)'입니다.

 

역운동학을 쉽게 이해하기 위해 순 운동학과 비교해서 이야기해보겠습니다.

 

순 운동학을 인체의 팔에 비유하자면, 관절에서부터 손끝까지 차례대로 계산하여 움직이는 방식입니다. 이와 반대로 역운동학은 손끝에서부터 관절로 역으로 계산하며 움직이는 방식입니다. 순 운동학의 경우 원인에서 결과로 절차적으로 계산해나가며 만들어가는 방식이라면, 역운동학은 다른 데이터로부터 역추적하여 데이터를 회복한다는 느낌입니다. (주관적인 느낌입니다)

 

역운동학을 흔히 볼 수 있는 곳은 바로 로봇공학과 애니메이션입니다. 그중 유니티를 배우고 있는 저희에게 익숙한 것은 '로봇공학'쪽일테니 그쪽을 중심으로 생각해보겠습니다. 예를 들어 3D RPG 게임에서 로봇이 있습니다. 저희는 이 로봇의 팔 동작을 컨트롤하고 싶은 상황입니다. 이럴 때 손끝부터 움직이며 동작을 컨트롤하는 역운동학을 이용해야 합니다.

 

왜냐면 저희는 현실에서도 손끝만 움직이기 때문입니다. 괜히 머릿속으로 "관절을 이렇게 이렇게 움직여서 손끝을 저기까지 뻗어야겠다"라고 생각하지 않습니다. 때문에 유니티에서도 팔 동작을 컨트롤할 때 역운동학을 이용하여 움직여주는 게 직관적으로 이해되며 컨트롤하기 편합니다.

 

관절이 들어간 모든 곳에 IK가 사용될 수 있기 때문에 그 범용성도 어마어마합니다. 이걸 어떻게 유니티에 적용하고, 이해하고, 응용할지 생각해본다면 앞으로 게임 개발을 할 때를 포함 등등 여러 상황에서 중요한 지적 자산이 되리라 생각합니다.

 

(2) FABR IK란

역운동학의 구현은 다양한 방식이 있습니다. 그 중 저희가 사용할 역운동학 알고리즘은 Foward and Backward Reaching Inverse Kinematic입니다. 줄여서 FABRIK(파브릭 알고리즘)이라고 부릅니다.

 

FABRIK이란 이름의 의미를 알아보겠습니다. IK에는 다양한 알고리즘 풀이들이 존재합니다. 이 중 가장 심플하고 빠른 알고리즘이 FABRIK이라 합니다. 보통 IK를 계산하는 과정에서 타깃점이 일정 범위 뒤로 이동했을 때 관절의 계산에 문제점(?)이 생기면서 문제풀이가 복잡해진다 합니다. 하지만 FABRIK는 이 문제점을 단지 '앞으로 뒤로 뻗는 방식(Forward and Backward Reaching)'을 통해 문제풀이를 단순화했습니다. 그래서 이름이 FABRIK입니다.

 

참고로 말씀드리자면 FABRIK은 성능면에서 다른 IK알고리즘보다 상당히 가볍고 좋습니다.

 

FABR IK의 작동원리

앞서 FABR은 Forward and Backward Reaching의 약자라고 말씀드렸습니다. 작동 원리도 이름과 동일하게 생각할 수 있습니다. 작동은 forward와 backward부분으로 나누어집니다. 일단 Forward reaching에 대해 살펴보겠습니다. 아래의 이미지를 봐주세요. 보면서 이게 어떤 방식으로 유니티에서 작동될지 생각해보겠습니다.

 

FABRIK의 Foward방식의 풀이.

 

상황을 요약하면, 하나의 라인이 있고 꼬리 부분과 머리 부분이 있습니다. 그리고 마우스로 클릭한 지점에 머리 부분이 이동하면서 꼬리 부분이 자연스럽게 따라오게 됩니다.

 

그럼 위 내용을 순서대로 분석해보겠습니다.

 

첫 번째 순서에서 마우스가 한 지점을 찍습니다. 이때 저장해야 하는 것은 아래와 같습니다.

//첫 번째 관절과 두 번째 관절의 사이 길이;
float length1 = Vector3.Distance(관절1, 관절2);
//첫 번째 관절과 마우스 클릭지점의 방향
Vector3 dir = 마우스클릭지점.position - 관절1.position;
//첫 번째 관절과 마우스 클릭지점과의 길이
float length2 = Vector3.Distance(관절1, 마우스클릭지점)

 

두 번째 순서에서 두 번째 관절을 마우스 클릭 지점으로 이동시킵니다. 

//그리고 위 길이만큼 처음 구했던 마우스 클릭지점의 방향으로 전진
관절2.position = 마우스클릭지점.position;

 

세 번째 순서에서 계산이 완료됩니다. 그 과정은 아마 아래와 비슷할 겁니다.

//마우스 클릭으로 인해 늘어난 길이
float length3 = length2 - length1;
//늘어난 길이만큼 관절1이 이동
관절1.position += dir * length3;

참고로 여기서 방향(dir)은 정규화(normalize)를 해줘야 합니다.

 

 

아래는 Foward에 대해 외국 사이트에서 소개되는 코드입니다. 

function reach(head, tail, tgt){
  // returns new head and tail in the format of:
  //   [new_head, new_tail]
  // where `new_head` has been moved to `tgt`

  // calculate the current length
  // (in practice, this should be calculated once and saved,
  //  not re-calculated every time `reach` is called)
  var c_dx = tail.x - head.x;
  var c_dy = tail.y - head.y;
  var c_dist = Math.sqrt(c_dx * c_dx + c_dy * c_dy);

  // calculate the stretched length
  var s_dx = tail.x - tgt.x;
  var s_dy = tail.y - tgt.y;
  var s_dist = Math.sqrt(s_dx * s_dx + s_dy * s_dy);

  // calculate how much to scale the stretched line
  var scale = c_dist / s_dist;

  // return the result
  return [
    // copy the target for the new head
    { x: tgt.x, y: tgt.y },

    // scale the new tail based on distance from target
    { x: tgt.x + s_dx * scale, y: tgt.y + s_dy * scale }
  ];
}

 

여기선 피타고라스 정의를 이용해서 관절의 길이(빗변)를 구합니다. 그리고 뻗쳐진 길이와 빗변을 비교하여 비율(scale)을 구했습니다. 이 비율은 기존의 빗변이 뻗쳐진 길이가 비해 얼마나 작은지 나타냅니다. 그리고 이 비율을 뻗쳐진 길이에 곱하면 head의 위치가 뻗쳐진 지점으로부터 몇만큼 뒤에 있는지 구할 수 있습니다. 이해가 어려워서 그림을 그려보고 이해했네요. 어쨌든 논리의 순서는 앞서 설명드린 것과 크게 다른 점은 없습니다. (이해가 어려우면 패스하시고 유니티로 따라 만들어보세요!)

 

그리고 위 코드는 상당히 직진적(?)이지만 중요한 포인트는 이 함수는 항상 성공한다는 점입니다. 라인은 항상 타깃에 닿기 위해 움직입니다.

 

(3) 유니티로 구현

이번 시간에 유니티로 구현해볼 동작

위와 같은 동작을 유니티로 구현해보겠습니다.

 

맵의 구성은 자유롭게 해 주세요. 타깃 지점이 될 오브젝트, 머리(Head) 부분이 될 오브젝트, 꼬리(Tail) 부분이 될 오브젝트 이렇게 3개의 오브젝이 필요합니다.

 

그리고 Head의 아래에 Tail을 Child화 시켜주세요. 

 

그리고 씬뷰에 기즈모를 그려주겠습니다. IK를 다룰 스크립트를 만들고 아래의 코드를 추가해주세요. 만든 스크립트는 Head에 넣어주시면 됩니다.

 

    void OnDrawGizmos()
    {
        //Head와 Tail의 거리를 구합니다.
        float dist  = Vector3.Distance(transform.GetChild(0).position, transform.position);

        //거리의 0.1만큼의 비율을 구합니다.
        float scale = dist * 0.1f;

        //꼬리로 향하는 방향을 구합니다.
        Vector3 dirToTail = transform.GetChild(0).position - transform.position;

        //Y축을 항상 꼬리방향을 보게합니다.
        Quaternion rotToChild = Quaternion.FromToRotation(Vector3.up, dirToTail);

        //꼬리까지 이어지는 gizmo의 모양을 구성합니다.
        Vector3 gizmoShape = new Vector3(scale, dist, scale);

        //지금까지 나온 재료를 가지고 기즈모를 그려냅니다.
        Handles.matrix = Matrix4x4.TRS(transform.position, rotToChild, gizmoShape);

        //기즈모의 색상을 초록색으로 바꿉니다.
        Handles.color = Color.green;

        //matix에 저장한 기즈모를 불러옵니다. matrix도형의 pivot지점과 스케일을 변경할수있습니다.
        //여기서 Vector3.up을 사용한 이유는 위에서 Y축을 꼬리방향으로 향하게 했기 때문입니다.
        Handles.DrawWireCube(Vector3.up * 0.5f, Vector3.one);
    }

 

Hanles.matrix = Matrix4x4.TRS()를 이용하여 기즈모를 그려냈습니다. 

 

그리고 아래의 코드를 작성합니다.

    Transform head;
    Transform tail;

    Vector3 headPos;  
    Vector3 tailPos;
    Vector3 dirToTarget;

    public Transform target;
    float betweenLength;

    private void Awake()
    {
        head = transform;
        tail = transform.GetChild(0);
        betweenLength = (head.position - tail.position).magnitude;
    }

사용할 head와 tail을 미리 받아옵니다. 또 중요한 포인트는 두 오브젝트 간의 길이를 미리 저장해놔야 합니다. 사이 거리는 상수로서 이후에 변할 일이 없습니다.

 

    void Update()
    {
        UpdateIK();
    }

    private void UpdateIK()
    {
        headPos = head.position;
        tailPos = tail.position;
        dirToTarget = (target.position - tail.position).normalized;

        headPos = target.position;
        tailPos = headPos - dirToTarget * betweenLength;

        head.position = headPos;
        tail.position = tailPos;
    }

이어서 업데이트 문을 작성합니다. 먼저 타깃 방향을 구합니다. 그리고 Head의 위치는 타깃 지점에 위치시킵니다. 꼬리의 위치는 headPos에서 betweenLength만큼의 거리를 빼 주기만 하면 됩니다. 그리고 계산을 마무리합니다.

 

 

완료
비교

정상적으로 작동합니다. 이상입니다. 다음 글에선 관절이 여러 개 있을 때 어떡해야 하는지 알아보겠습니다.