스켈레탈 애니메이션
- 게임에서 캐릭터 애니메이션은 가상의 뼈대인 본(Bone)을 캐릭터 메시에 심은 후, 해당 본의 움직임에 맞춰 캐릭터 메시가 변형되는 방식을 사용한다.
- 이러한 방식을 스켈레탈 애니메이션(Skeletal animation)이라고 부른다.
- 스켈레탈 애니메이션을 구현하려면 기존 메시 체계를 확장해 본을 추가해야한다.
- 본은 단순히 이동만 하는 것이 아니고 회전하거나 크기도 변경되므로 본의 정보는 트랜스폼에서 관리한다.
- 본을 구성하려면 3가지 정보가 필요하다.
- 본이 움직일 때 상대적인 변화량을 파악하는데 기준으로 사용하는 바인드보즈(Bindpose)정보
- 본의 트랜스폼 정보를 저장할 공간
- 본을 식별할 수 있는 고유한 이름 정보
- 배경으로 사용되는 애니메이션이 없는 고정된 메시는 본 정보가 필요하지 않다.
- 하지만 애니메이션이 필요한 캐릭터 메시에는 본 정보가 있어야한다.
- 본이 심어진 메시를 스킨드 메시(Skinned mesh)라고 부른다.
- 스킨드 메시를 구축한다면 아래와 같이 기존 메시를 확장해 본을 심을 수 있다.
- 메시에 본 정보를 추가한 후에는 본의 움직임에 따라 메시의 정점이 함께 움직이도록 본과 정점을 연결해야 한다.
- 이 작업을 리깅(Rigging)이라 부른다.
- 리깅은 뼈에 살을 붙이는 작업이라고 할 수 있다.
- 리깅 작업을 데이터 관점에서 살펴본다면 다음과 같다.
- 하나의 정점에 대해 어떤 본들이 영향을 미치는지를 기록하는 것
- 정점에 영향을 미치는 본이 많을수록 메시는 세심하게 변화할 것
- 하지만 메시에 영향을 미치는 본이 많아질수록 정점의 최종 위치를 구하는 계산량도 그만큼 늘어나게 된다.
- 리깅을 구현하기 위해서는 메시의 구조를 조금 확장해야 한다.
- 정점마다 몇 개의 본으로부터 영향을 받는지 개수를 지정해야 함
- 영향을 주는 본들은 서로 얼마나 영향력을 주는지 그 비율에 대한 별도의 정보가 필요함
- 한 정점에 영향을 주는 본들의 연결 정보를 웨이트(Weight)라 부른다.
- 리깅을 구현하기 위해 위 그림의 구조를 확장해 정점마다 연결된 본의 수와 연결된 본의 이름, 웨이트 정보를 추가한 내용은 아래 그림과 같다.
- 스킨드 메시 설정이 완료되면 스키닝 애니메이션의 진행을 위해 시간에 따라 본을 움직여줘야 한다.
- 본의 움직임을 구현하기 위해, 게임 엔진은 시간에 따라 값의 변화를 저장한 키프레임(Keyframe) 데이터를 사용한다.
- 키프레임은 점이 찍힌 주요 지점에서 값을 지정하면 나머지 중간 값은 관련 수식을 통해 보간하여 지정한 시간 동안 끊김 없는 데이터를 제공하도록 구성되어 있다.
- 캐릭터 애니메이션은 게임 오브젝트의 모든 상태가 최종 결정된 게임 로직이 완료된 시점에 진행되어야 한다.
- 숨쉬기와 걷기 두 가지 애니메이션이 설정된 케릭터가 있고, 정지된 상태에서 숨쉬기 애니메이션이 재생되고 있고, Y축 입력을 통해 캐릭터가 앞으로 이동하고 있는 상황을 가정
- Y축 입력을 처리하기 전에 애니메이션을 처리한다면, 현재 케릭터가 멈춘 것으로 판단해 애니메이션 시스템은 숨쉬기 애니메이션을 선택할 것
- 해당 프레임에서 캐릭터는 숨쉬는 모션을 재생하면서 앞으로 전진하게 된다.
트랜스폼 계층 구조
- 일반적으로 캐릭터를 구성하는 본은 모두 유기적으로 연결되어 있으며, 부모-자식 관계라고 불리는 계층 구조가 형성되어 있다.
- 손가락이 움직이지 않아도 팔목이 움직이면 손가락의 최종 위치가 달라지듯이 게임 캐릭터도 부모-자식의 계층 구조를 가져야 실제로 사람이 움직이는 것처럼 구현할 수 있다.
- 부모-자식 계층 관계에서 부모 트랜스폼과 자식 트랜스폼이 가져야 하는 규칙을 정리하면 다음과 같다.
- 부모가 움직이면 부모의 움직임만큼 자식도 움직인다.
- 자식이 움직여도 부모는 영향을 받지 않는다.
- 하나의 부모는 여러 자식을 가질 수 있다.
- 자식은 하나의 부모만 가질 수 있다.
- 계층 구조에서 부모가 없는 최상단 트랜스폼을 루트(Root) 트랜스폼이라고 한다.
- 트랜스폼에서 부모-자식 관계가 형성되면 자식 트랜스폼 데이터는 부모로부터 상대적인 트랜스폼과 자식이 속한 공간에 대한 트랜스폼, 두 가지로 분리해 관리해야 한다.
- 부모로부터 상대적인 트랜스폼을 로컬 트랜스폼(Local transform)이라 부른다.
- 계층 구조와 상관없이 자신이 속한 공간에서의 트랜스폼을 월드 트랜스폼(World transform)이라 부른다.
- 만약 자신이 루트 트랜스폼이면, 로컬 트랜스폼과 월드 트랜스폼의 값은 같다.
- 월드 트랜스폼은 절대 트랜스폼(Absolute transform), 로컬 트랜스폼은 상대 트랜스폼(Relative transform)이라는 용여로도 사용된다.
- 트랜스폼을 두 가지로 분리하는 이유
- 계층 구조가 변동되는 다양한 상황에 효과적으로 대처할 수 있기 때문
- 부모로부터 상대적인 정보를 기록한 로컬 트랜스폼은 계층 구조를 다룰 때 유용하게 사용된다.
- 로컬 트랜스폼의 정보를 보관하지 않고 월드 트랜스폼 정보만 보관한다면, 트랜스폼의 변동이 있을 때 마다 부모와 나의 월드 트랜스폼을 비교해 로컬 트랜스폼을 매번 계산해줘야한다.
트랜스폼 계층 구조의 변환
- 월드 트랜스폼과 로컬 트랜스폼 값이 일관되게 하려면 어떤 점들이 고려햐여 하는지 부모와 자식이 가진 트랜스폼을 중심으로 사례를 정리했다.
- 월드 트랜스폼과 로컬 트랜스폼을 동시에 다루는 경우 고려해야 할 다양한 사례를 확인할 수 있다.
- 계층 구조에 관한 문제를 구현하기 위해서는 다음과 같은 두 가지 기능이 필요함을 알 수 있다.
- 부모의 월드 트랜스폼 정보와 나의 로컬 트랜스폼 정보를 사용해 나의 월드 트랜스폼을 계산하는 기능
- 부모의 월드 트랜스폼 정보와 나의 월드 트랜스폼 정보를 비교해 부모로부터 상대적인 나의 로컬 트랜스폼을 계산하는 기능
사레 1 : 부모의 월드 트랜스폼이 변경된 경우
- 부모의 월드 트랜스폼이 변경되는 경우 나의 로컬 트랜스폼은 그대로 유지되지만 나의 월드 트랜스폼은 변경된다.
- 변경된 나의 월드 트랜스폼은 부모의 변경된 월드 트랜스폼 정보와 나의 로컬 트랜스폼을 사용해 계산할 수 있다.
- 이는 나의 자식에게도 동일한 상황이 되어 나와 자식과의 로컬 트랜스폼은 그대로 유지되지만 모든 자식들의 월드 트랜스폼은 변경되어야 한다.
- 위 상황은 다음 그림에 정리되어 있다.
- 오른쪽 그림의 주황색 선은 변경된 트랜스폼을 의미하며, 트랜스폼의 변경 순서는 숫자로 나타냈다.
사례 2 : 나의 로컬 트랜스폼이 변경된 경우
- 부모는 가만히 있는데 나의 로컬 트랜스폼이 변경되는 경우, 나의 월드 트랜스폼도 변경된 로컬 트랜스폼에 맞춰서 변경해야 한다.
- 이 경우 사례 1과 동일하게 부모의 월드 트랜스폼 정보와 변경된 나의 로컬 트랜스폼을 사용해 나의 월드 트랜스폼을 다시 계산해야 한다.
- 나의 월드 트랜스폼이 변경되면 사례 1과 동일하게 자식에게도 영향을 미친다.
- 따라서 자식의 로컬 트랜스폼은 변화가 없지만, 모든 자식의 월드 트랜스폼은 변경되어야 한다.
사례 3 : 나의 월드 트랜스폼이 변경된 경우
- 사례 3은 사례 2와 유사하다.
- 부모는 가만히 있는데 나의 월드 트랜스폼이 변경되는 경우 로컬 트랜스폼도 변경된 월드 트랜스폼에 따라 변경해야 한다.
- 그런데 이때는 부모의 월드 트랜스폼과 변경된 나의 월드 트랜스폼을 비고ㅛ해 부모로부터 상대적인 로컬 트랜스폼을 계산해야 한다.
- 나의 월드 트랜스폼이 변경됐으니 사례 1과 동일하게 자식의 월드 트랜스폼을 변경한다.
- 다음 그림은 사례 2의 그림과 구성은 동일하지만 계산 순서가 다르다.
사례 4 : 기존에 설정된 부모를 버리고 새로운 부모로 편입하는 경우
- 이 경우는 두 단계로 나눠 살펴봐야 한다.
- 첫 번째 단계에서는 나를 부모로부터 독립시키고 최상단의 루트 트랜스폼으로 만들어준다.
- 내 트랜스폼이 루트 트랜스폼이 되면 월드 트랜스폼괴 로컬 트랜스폼은 같은 값을 가지므로 나의 월드 트랜스폼 데이터를 로컬 트랜스폼에 덮어쓰는 것으로 해결할 수 있다.
- 나의 월드 트랜스폼은 변경된 적이 없으므로 자식의 트랜스폼을 변경할 필요는 없다.
- 첫 번째 단계는 다음 그림으로 정리할 수 있다.
- 두 번째 단계에서는 다를 새로운 부모의 자식으로 등록한 후, 새로운 부모와 나 사이의 로컬 트랜스폼을 계산한다.
- 이때 새로운 부모의 월드 트랜스폼과 나의 월드 트랜스폼을 비교해 새로운 부모로부터 상대적인 로컬 트랜스폼을 계산해야 한다.
- 이 단계에서도 나의 월드 트랜스폼은 변한 적이 없으므로 자식의 트랜스폼을 변경할 필요는 없다.
- 두 번째 단계를 정리하면 다음 그림과 같다.
로컬 트랜스폼으로부터 월드 트랜스폼의 계산
- 나의 월드 트랜스폼의 크기는 부모의 월드 트랜스폼의 크기와 나의 로컬 트랜스폼의 크기를 곱한 값이 된다.
- 부모의 월드 크기가 20이고, 내 로컬 크기가 0.5라면 나의 월드 크기는 10이 될 것이다.
- 크기를 구성하는 세 축은 모두 직교하므로 각 축의 크기는 독립적으로 작용된다.
- 부모의 월드 크기를 벡터 s = (s_x, s_y, s_z)로 지정하고 나의 로컬 크기를 벡터 s' = (s'_x, s'_y, s'_z)로 지정한다면 나의 월드 크기 s'_world = (s_x·s'_x, s_y·s'_y, s_z·s'_z)가 된다.
두 벡터 v_1 = (x_1, y_1, z_1)과 v_2 = (x_2, y_2, z_2)의 곱셈을 다음과 같이 정의하면
v_1 * v_2 = (x_1, y_1, z_1)*(x_2, y_2, z_2) = (x_1·x_2, y_1·y_2, z_1·z_2)
나의 월드 크기는 다음 수식으로 정리할 수 있다.
s'_world = s*s'
- 부모의 월드 회전을 사원수 q가 담당하고 나의 로컬 회전을 사원수 q'가 담당한다면, 나의 월드 회전은 다음과 같이 두 사원수의 곱으로 계산된다.
q'_world = q'·q
- 부모 월드 트랜스폼의 세 로컬 축을 각각 x = (x_x, x_y, x_z), y = (y_x, y_y, y_z), z = (z_x, z_y, z_z)로 설정하고 이동할 값을 벡터 t = (t_x, t_y, t_z)로 설정하면 부모 월드 트랜스폼으로부터 모델링 행렬 M_parent는 다음과 같이 생성된다.
┌ x_x·s_x y_x·s_y z_x·s_z t_x ┐
M_parent = | x_y·s_x y_y·s_y z_y·s_z t_y |
| x_z·s_x y_z·s_y z_z·s_z t_z |
└ 0 0 0 1 ┘
- 내 로컬 트랜스폼의 세 로컬축을 각각 x', y', z'으로 설정하고 이동할 값을 벡터 t' = (t'_x, t'_y, t'_z)로 설정하면 내 로컬 트랜스폼의 모델링 행렬 M_local은 다음과 같이 얻을 수 있다.
┌ x'_x·s'_x y'_x·s'_y z'_x·s'_z t'_x ┐
M_local = | x'_y·s'_x y'_y·s'_y z'_y·s'_z t'_y |
| x'_z·s'_x y'_z·s'_y z'_z·s'_z t'_z |
└ 0 0 0 1 ┘
- 나의 월드 트랜스폼의 행렬 M_world는 다음과 같은 행렬 곱으로 계산된다.
M_world = M_parent · M_local
┌ x_x·s_x y_x·s_y z_x·s_z t_x ┐ ┌ x'_x·s'_x y'_x·s'_y z'_x·s'_z t'_x ┐
M_world = | x_y·s_x y_y·s_y z_y·s_z t_y |·| x'_y·s'_x y'_y·s'_y z'_y·s'_z t'_y |
| x_z·s_x y_z·s_y z_z·s_z t_z | | x'_z·s'_x y'_z·s'_y z'_z·s'_z t'_z |
└ 0 0 0 1 ┘ └ 0 0 0 1 ┘
- 위에서 구한 M_world의 마지막 4열 값이 월드 트랜스폼의 이동 값이 될 것이다.
- 따라서 모두 다 곱하지 않고 4열에 대한 값만 내적과 벡터의 곱셈을 통해 정리하면 이동 값은 다음 식과 같다.
t'_world = ((x_x, y_x, z_x)·(t'*s), (x_y, y_y, z_y)·(t'*s), (x_z, y_z, z_z)·(t'*s))+t
┌ x_x y_x z_x ┐ ┌ t'_xs_x ┐
= | x_y y_y z_y | | t'_ys_y | + t
└ x_z y_z z_z ┘ └ t'_zs_z ┘
= (q·(t'*s)) + t
월드 트랜스폼으로부터 로컬 트랜스폼의 계산
- 부모의 월드 트랜스폼 정보와 나의 월드 트랜스폼 정보를 비교해 나의 로컬 트랜스폼을 계산하기 위해서는 부모의 월드 트랜스폼과 나의 월드 트랜스폼의 차이를 계산해야 한다.
- 이를 위해서는 월드 공간을 카메라 중심의 뷰 공간으로 바꾼 행렬을 이용해야 한다.
- 만일 부모의 월드 트랜스폼을 중심으로 나의 월드 트랜스폼을 해석할 수 있다면 그 값이 나의 로컬 트랜스폼이 될 것이다.
- 이를 위해서는 카메라 트랜스폼에 역변환을 적용한 것 처럼 부모의 월드 트랜스폼에 역변환을 반영하면 된다.
- 크기의 역변환은 크기의 역수로 계산된다.
크기 벡터 s = (s_x, s_y, s_z)를 역변환하면
s^-1 = (1/s_x, 1/s_y, 1/s_z)
- 크기를 배제한 트랜스폼 회전을 q로 지정하면 회전의 역변환은 켤레 사원수 q^*가 된다.
- 마지막으로 이동 값 t^-1을 구하기 위해선 트랜스폼을 담당하는 모델링 행렬의 역행렬을 계산해야한다.
- 모델링 행렬의 M^-1은 각 행렬의 역행렬을 반대 순서로 곱한 값이 된다.
M^-1 = (TRS)^-1 = S^-1 R^-1 T^-1
┌ 1/s_x 0 0 0 ┐
S^-1 = | 0 1/s_y 0 0 |
| 0 0 1/s_z 0 |
└ 0 0 0 1 ┘
┌ x_x x_y x_z 0 ┐
R^-1 = | y_x y_y y_z 0 |
| z_x z_y z_z 0 |
└ 0 0 0 1 ┘
┌ 1 0 0 -t_x ┐
T^-1 = | 0 1 0 -t_y |
| 0 0 1 -t_z |
└ 0 0 0 1 ┘
┌ x_x/s_x x_y/s_x x_z/s_z (-x·t)/s_x ┐
M^-1 = | y_x/s_y y_y/s_y y_z/s_z (-y·t)/s_y |
| z_x/s_z z_y/s_y z_z/s_z (-z·t)/s_z |
└ 0 0 0 1 ┘
- 모델링 행렬의 역행렬 M^-1에서 위치에 대한 4열의 값 t^-1은 다음과 같이 계산된다.
t^-1 = (x·(-t/s_x), y·(-t/s_y), z·(-t/s_z))
= (x·(-t), y·(-t), z·(-t))*(s^-1)
┌ x_x x_y x_z ┐ ┌ -t_x ┐
= | y_x y_y y_z |·| -t_y | * (s^-1)
└ z_x z_y z_z ┘ └ -t_z ┘
= (q^*·(-t))*(s^-1)
- 역변환 기능을 사용해 부모의 월드 트랜스폼의 역변환을 구한 후에 나의 월드 트랜스폼을 곱하면 나의 로컬 트랜스폼이 만들어진다.
M_local = M^-1_parent · M_world
- 부모 트랜스폼이 역변환된 행렬도 트랜스폼의 일종이므로 로컬 트랜스폼에서 월드 트랜스폼으로 변환과 동일하게 계산하면 된다.
- 부모의 역변환된 트랜스폼을 구성하는 요소는 다음과 같다.
- 크기 S^-1 = (1/s_x, 1/s_y, 1/s_z)
- 회전 = q^*
- 이동 = t^-1
- 부모의 월드 트랜스폼을 기준으로 상대적인 나의 로컬 트랜스폼을 구하는 수식은 다음과 같다.
나의 월드 트랜스폼의 크기를 s' = (s'_x, s'_y, s'_z)이면 나의 로컬 트랜스폼의 크기는
s'_local = s^-1 * s' = (s'_x/s_x, s'_y/s_y, s'_z/s_z)
나의 로컬 트랜스폼의 회전 q'_local은
q'_local = q^* · q'
나의 로컬 트랜스폼의 위치는
t'_local = q^* · (t'*s^-1) + (t^-1)
캐릭터 메시와 애니메이션
- 트랜스폼의 계층 구조를 이루는 캐릭터를 제작하기 위해선 캐릭터 메시를 생성하고, 계층 구조로 본을 생성한 후에 리깅을 진행해야 한다.
- 캐릭터를 구성하는 본의 계층 구조는 흔히 캐릭터의 시작 위치를 나타내는 루트(Root)에서 출발해 골반(Pelvis)으로 이동한 뒤 여기에서 부터 척추(Spine), 팔(Arm), 다리(Leg), 목(Neck)으로 퍼져나가는 구조를 띈다.
- 케릭터를 구성하기 위해서는 본도 계층 구조를 지원해야 한다.
- 계층 구조로 본을 설정한 후에는 각 본과 정점을 연결하는 리깅 작업을 진행해 캐릭터 애니메이션을 진행할 스킨드 메시를 완성시킨다.
출처
https://m.yes24.com/Goods/Detail/107025224
'게임수학' 카테고리의 다른 글
[게임수학] 사원수(Quaternion) (0) | 2024.05.06 |
---|---|
[게임수학] 극한과 오일러 공식 (0) | 2024.05.03 |
[게임수학] 복소수 (0) | 2024.05.03 |
[게임수학] 모델링과 뷰 (0) | 2024.04.28 |
[게임수학] 절두체(Frustum) (0) | 2024.04.25 |