수학/게임수학

게임 수학 17장 : 캐릭터 (게임에 생기를 불어넣는 기술)

cyphen156 2025. 7. 14. 18:04

이제 이 책의 마지막 장이다. 

17장에서는 지금까지 배운 내용들을 종합하여 가상의 3차원 공간에서 움직이는 캐릭터를 구현하는 방법에 대해서 알아본다고 한다.

스켈레탈 애니메이션

보통 게임에서 캐릭터의 움직임은 "애니메이션이라는 기법"을 통해 표현된다.

이 애니메이션을 위해 필요한 기술이 캐릭터를 구성하는 부분 부분(머리, 상체, 팔, 다리 등)을 각각 따로 만든 뒤, 이들을 연결하는 가상의 뼈대인 본캐릭터 메시에 심어 본의 위치(Position)와 스케일(Scale),  회전값(Roatation)에 따라 캐릭터의 구성하는 부품의 메시를 변형하는 방식을 사용하는데, 이것을 스켈레탈 애니메이션이라고 부른다.

본은 게임 캐릭터의 움직임을 제어하기 위해 설정된 가상의 뼈 구조로, 각 부위 간의 연결관계(계층 구조)를 표현한다

본 시스템의 핵심은 본 사이의 연결성과 가중치(Weight)이다.

  • 하나의 본이 움직이면, 그 본에 직접 연결된 자식 본들도 함께 영향을 받아 트랜스폼(위치, 회전, 스케일)이 변경된다.
  • 처음의 변형은 연결 정보를 따라 전달되어, 최초로 움직인 본의 정보가 이후 본들에도 연쇄적으로 적용됩니다.

즉, 본의 움직임은 트리 구조를 따라 퍼지며, 이로 인해 캐릭터 메시가 자연스럽게 변형된다. 이것이 바로 스켈레탈 애니메이션의 기본 동작 원리이다.

본을 구성하는 3가지 정보

  • 고유 식별 이름 : 각 본을 식별하기 위한 고유한 이름
  • 바인드 포즈 : 캐릭터 매시에 처음 본이 심어질 때 저장되는 본의 배치정보, 초깃값
  • 트랜스폼 정보 : 모델링 또는 애니메이션 행렬(Matrix)에 의해 지속적으로 갱신되는 현재의 본의 트랜스폼 정보

모든 오브젝트에 본을 사용하면 좋겠지만, 위에서 보여준 3가지 정보 외에도 연결 관계, 연쇄 작용 여부 및 가중치 등 다양한 정보들을 포함시켜야 하기 때문 본은 애니메이션을 수행해야할 대상에게 한정하여 적용하는것이 효율적이다. 

-> 대부분의 오브젝트에는 본 정보가 포함되지 않는 것이 좋다는 소리이다.

본은 다음과 같이 메시 구조체스킨드 메시로 확장해 사용한다.

리깅(Rigging)

메시에 본의 정보를 추가했다면 다음은 본과 메시의 정점들이 서로 영향을 받도록 연결하는 작업이 필요한데 이것을 리깅이라고 부른다.

본 하나여러개의 정점에 영향을 줄 수 있고, 하나의 정점하나의 본에만 영향을 받는게 아닌 여러개의 본에 의해 영향을 받을 수 있다 대 다 관계를 갖는다. 그래서 본이 많을수록 디테일한 움직임 묘사가 가능해 지지만 그만큼 연산량이 급격하게 증가하기 때문에 적절한 타협이 필요하다.

다음과 같이 메시 구조에 정점 버퍼에 본과의 연결 정보를 추가한 메시 구조체

보통 리깅은 별거 아닌것 같고 귀찮은데 잘못하면 캐릭터에 대한 표현이 한순간에 망가진다. 그래서 따로 리깅만 전문으로 하는 직군(리거)이 있을 정도로 중요한 작업이다. 만약 주변에 리깅 잘하는 사람 있다면 반드시 친해지자.

리깅시 메시에 적용되는 파생 가중치 시각화 예시

개인적으로 작업한 것 보여주고싶었는데 C4D 파일에 리깅 정보가 날아갓다... 대충 잘못하면 이렇게 된다는 것만 알아두자.

잘못된 리깅 정보로 인해 잘못 적용된 스케일 변환

이렇게 본과 정점을 리깅했다면 다음은 애니메이션의 진행을 위해 시간 또는 프레임에 따라 본을 움직여 주는 작업을 구현하게 되는데, 이것을 게임 엔진에서는 키프레임이라는 데이터를 사용하여 구현한다.

키프레임은 특정 시간마다 본의 트랜스폼 상태를 저장/확정하여 각 키프레임간 간격을 프레임 단위로 보간하여 애니메이션을 끊임없이 변화 시켜 데이터를 제공하도록 구성한다.

그런데 애니메이션은 언제 화면에 그려야 할까? 이벤트 발생 직후?, 게임 로직 수행 전?, 물리 연산 후? 그것도 아니면 모든 게임 로직 수행된 이후?

보통은 다음과 같이 게임 로직이 수행된다.

1. Updaete() : 이번 프레임에 들어온 이벤트(플레이어와의 상호작용)는 이벤트 큐에 쌓아 놓고, 다음 프레임에 적용하기 위해 처리를 대기시킨다. 

그렇다면 우리에게 남은 작업은 이전 프레임에 발생한 이벤트들을 이번 프레임 렌더링시 적용시키는 작업을 해야 할 것이다.

게임 엔진을 설계한 사람에 따라 처리 로직이 다르겟지만 보통 Update 단계에서 이번 프레임에서의 플레이어와의 상호작용을 마무리한다.

2. FixedUpdate : 오브젝트간 물리적 상호작용을 이벤트 순서에 따라 처리한다. 

3. LateUpde/FinalUpdate : 물리처리까지 완료된 게임오브젝트의 상태를 바탕으로 애니메이션/카메라에 비춰지는 정보를 마무리 업데이트한다.

4. Render() :CPU 또는 GPU에게 화면에 그릴 데이터를 전달해주고 모니터에 출력한다.

트랜스폼 계층 구조

트랜스폼의 계층 구조는 정의하기에 따라 다르지만 보통 부모-자식 관계를 다음과 같이 정의한다

  • 하나의 부모여러 자식을 가질 수 있다.
  • 자식하나의 부모만 가질 수 있다.

그리고 가중치에 따라 달라지는 관계는 다음과 같다.

  • 부모의 움직임에 따라 자식도 영향을 받아 가중치 만큼 보정하여 움직인다.
  • 자식의 움직임에 따라 부모도 가중치 만큼 보정하여 움직인다. 

그리고 부모가 없는 최상단의 트랜스폼(메인 트랜스폼)을 루트 트랜스폼이라고 부른다.

CK소프트렌더러는 다음과 같은 규칙을 가지고 있다.

  • 부모가 움직이며 부모의 움직임 만큼 자식도 움직인다. (가중치 1)
  • 자식이 움직여도 부모는 영향받지 않는다 (가중치 0)
  • 하나의 부모는 여러 자식을 가질 수 있다,
  • 자식은 하나의 부모만 가질 수 있다.

이렇게 계층 구조를 형성한 트랜스폼 데이터는 종속 관계에 의한 상대 트랜스폼자신이 속한 공간에 대한 트랜스폼 두 가지로 분리하여 관리하게 되는데, 루트 트랜스폼은 보통 종속 관계와 상관 없이 그 자체로 가상공간에서의 자신의 트랜스폼을 적용하기에 월드 트랜스폼이라고 부르며,층 구조를 갖는 트랜스폼은 로컬 트랜스폼이라고 부른다.

여기서 부르는 월드 트랜스폼로컬 트랜스폼3D캐릭터 한 모델에서만 정의된 트랜스폼 호칭으로, 가상 공간의 트랜스폼과 구분하기 위해 절대(Absolute) 트랜스폼 상대(Relative) 트랜스폼으로 부르기도 한다.

트랜스폼 계층 구조의 변화

1. 부모의 월드 트랜스폼이 변경된 경우

로컬 트랜스폼그대로 유지 되지만 절대 트랜스폼변경된다.

부모의 변경된 월드 트랜스폼 정보와 자식의 로컬 트랜스폼 정보를 사용해 나의 월드 트랜스 폼계산할 수 있다. 

2. 자식의 로컬 트랜스폼이 변경된 경우

나의 로컬 트랜스폼 변경된 경우에는 부모는 영향을 받지 않지만 나의 자식은 영향을 받는다. 

나의 월드 트랜스폼 정보부모의 월드 트랜스폼 정보나의 로컬 트랜스폼을 사용변경하게 되고, 나의 자식의 월드 트랜스폼은 이렇게 계산된 나의 월드 트랜스폼 정보를 이용해 변경하게 된다. 

3. 나의 월드 트랜스폼이 변경된 경우

나의 월드 트랜스폼이 변경된 경우에는 사례 1과 동일하게 변경 로컬 트랜스폼을 변경해줘야 하지만 계산순서가 달라진다.

4. 부모가 변경된 경우

4_1. 부모 오브젝트에서 떠나 독립하는 경우

나를 루트 트랜스폼으로 만들어 준다. 나의 월드 트랜스폼은 나의 로컬 트랜스폼과 같은 값을 가지게 되므로 월드 트랜스폼 정보를 로컬 트랜스폼 정보로 덮어씌운다.

자식 트랜스폼은 변경할 필요가 없다.

4_2. 새로운 부모 오브젝트에 종속되는 경우 

4_1을 통해 루트로 만든 나의 트랜스폼을 다시 새로운 부모 오브젝트에 종속시키고, 부모 오브젝트의 월드 트랜스폼 정보에 따라 나의 월드 트랜스폼 정보변경한다.

로컬 트랜스폼으로 월드 트랜스폼 계산하기

우리는 이렇게 종속관계에 따라 트랜스폼 정보가 다양하게 변경될 수 있다는 것을 알았다. 

우선 트랜스폼의 스케일 값의 경우 상대적인 비율로 계산된다. 만약 부모의 월드 스케일이 10이고 나의 로컬 스케일이 0.5라면, 나의 월드 스케일은 10 * 0.5인 5가 된다.

그리고 다른 변환 행렬인 이동, 회전의 경우도 동일하게 연산이 된다.

따라서 자식 트랜스폼의 월드 트랜스폼 모델링 행렬은 부모의 월드 트랜스폼의 모델링 행렬 * 자식의 로컬 트랜스폼 모델링 행렬이 된다.

월드 트랜스폼으로  로컬 트랜스폼계산하기

이번에는 부모 또는 나의 월드 트랜스폼 정보를 사용해 나의 로컬 트랜스폼을 계산하는 방법에 대해 알아보겠다. 

앞서 로컬 트랜스폼을 통해 월드 트랜스폼을 계산할 때 부모의 월드 트랜스폼나의 월드 트랜스폼나의 로컬 트랜스폼 값에 의해 행렬 곱을 통해 비율 연산이 수행된다는 것을 알았다.

이것은 이전에 공부했던 월드 공간을 카메라 중심의 뷰 공간으로 바꾼 행렬과 유사한 형태이다. 

그래서 역변환 행렬을 구하면 두 개의 월드 트랜스폼 정보를 통해 나의 로컬 트랜스폼 정보를 쉽게 구할 수 있다.

그리고 이런 역변환 행렬inverse()라는 함수를 통해 구현된다.

캐릭터 메시와 애니메이션

최종적으로 애니메이션을 구현하기 위해 따라야 할 작업 흐름을 정리해보도록 하겠다.

1. 캐릭터를 구성하기 위해서는 트랜스폼과 본이 계층 구조를 가져야 한다.

본을 계층 구조로 설정했다면 

2. 본과 정점을 연결하는 리깅 작업을 수행하여 스킨드 메시를 완성한다.

3. 완성된 메시를 애니메이션을 부드럽게 수행하기 위해 키프레임 데이터를 생성하여 이것을 통해 본이 회전하도록 설정한다.

4. 바인드 포즈에 설정된 트랜스폼 정보를 이용해 애니메이션에 의해 움직인 본의 변화량을 반영해 스킨드메시의 최종 정점 위치를 계산하여준다.

마지막으로 갈수록 대충 끝낸 감이 없지않아 있는데 예제는 다음주에 정리해보도록 하겠다.

그간 부족한 지식으로 최대한 나만 알아볼 수 있는게 아닌 다른사람도 이해할 수 있도록 정리해보려고 했는데 잘 됬는지는 모르겠다.

다음 할 공부는 아마도 컴퓨터 그래픽스 아니면 컴퓨터 사이언스 쪽이 될것 같은데 흠..... 안할수도있고 할수도 잇고?

내일 스터디 하고 후기나 올려야겠다.

모든 예제 코드의 소스파일은

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

 

GitHub - onlybooks/gamemath: <이득우의 게임 수학> 공식 깃허브 페이지

<이득우의 게임 수학> 공식 깃허브 페이지. Contribute to onlybooks/gamemath development by creating an account on GitHub.

github.com

또한 제 개인 깃허브 레포지토리 에 있습니다.

Workspace/C++/GameMath at main · cyphen156/Workspace · GitHub

 

Workspace/C++/GameMath at main · cyphen156/Workspace

Studying . Contribute to cyphen156/Workspace development by creating an account on GitHub.

github.com

※ 이득우 교수님의 인프런 게임수학강의를 참고하여 작성되었습니다.