애니메이션은 관절을 이용해서 구현한다.
메시 그 큰 덩어리의 정점 하나 하나의 변화를 추적해서 FBX에 기록하면 사이즈가 말도 안되게 커질 것이다(..)
위 사진과 같이 뼈대를 만들고 그 뼈대가 움직이면 뼈대와 연결된 살들도 같이 움직이는 식
이런 작업을 스키닝이라고 한다.
예제에서 정확한 표현은 관절을 통해 움직이는 것인데,
관절이 움직이면 그 관절이 움직일 때 뼈도 움직이니까 그 뼈에 붙은 정점들도 같이 움직이는 식.
그리고 그 관절들은 어깨의 자식은 팔이고, 팔의 자식은 손과 같은 방식으로 부모 자식 계층 관계를 가지고 있다.
어깨가 움직이면 간접적으로 팔이 움직이고 팔이 움직이면 손이 움직이는 것과 같은 이치.
이 관절 하나 하나를 노드라고 부르며, 하나의 노드 아래에 여러 노드가 붙을 수 있다.
(Ex. 몸통에 다리 2개가 붙어 있는 것처럼)
따라서 이런 구조를 구현하기에 트리 구조만큼 적합한 구조가 없다.
최초의 루트 노드에서 나무를 뒤집어 놓은 것처럼 연결된 구조가 되는 것
루트 노드는 보통 사람으로 치면 몸통을 루트 노드라고 부르며, 루트 노드는 부모가 없으므로 월드 좌표를 기준으로
좌표가 배치된다.
그런데 이런 방식으로 애니메이션을 구현하면 또 하나의 문제가 생기는게,
우리가 만약 허리를 움직인다고 가정하면 허리 뿐만 아니라 가슴, 골반도 조금씩 같이 움직인다.
자연스러운 애니메이션을 구현하기 위해서는 이렇게 한 관절이 움직일 때 다른 관절도 영향을 받는 것을 구현해야 하는데,
이 예제에서는 가중치라는 개념을 둬서 구현했다.
한 관절이 영향을 주는 최대 4개의 관절을 알고 있고 각각 가중치를 둬서 관절이 움직일 때 연결된 관절 각각에 (최종 행렬 * 0~1)사이의 가중치를 적용하는 것.
애니메이션의 계층 구조
이런 방식을 구현하기 위해서는 자식이 부모의 좌표계에 속하도록 구현하게 되는데 (상대 좌표)
결국 이 자식이 화면에 렌더링되기 위해서는 월드에서의 좌표가 필요하다.
결국 메쉬는 월드 좌표를 기준으로 화면에 그려지고, 자식의 자식의 자식의 자식이라도 결국
그 월드 좌표를 기준으로 위치값이 존재해야 한다.
이 위치값은 어떻게 구해야 하는지 생각해보면,
자식의 부모는 또 부모가 있고 타고 타고 올라가면 결국 루트 노드가 나온다.
루트 노드는 월드 좌표 기준으로 자신의 위치를 가지고 있기 때문에,
자식의 자식의 자식도 루트 노드를 기준으로 한 좌표계로 변환해주면 된다는 결론이 나온다.
변환해주는 행렬에 대해 용어 정리를 잠깐 하면
ToParent = 자신의 좌표를 부모의 월드로 이동시키는 행렬
ToRoot = 자신의 좌표를 월드 좌표 직전의 루트의 좌표계로 이동시키는 행렬
ToWorld = 자신의 좌표를 월드 좌표로 이동시키는 행렬 (결국 루트를 거치게 되는)
F=관절, A=행렬
F₁의 자식이 F₂
F₂의 자식이 F₃
A₃는 F₃를 F₂의 좌표계를 이동시키는 행렬
A₂은 F₂을 F₁으로 이동시키는 행렬
위 식에 따르면 Fᵢ에서 월드 행렬을 구하려면
ToWorldᵢ = Aᵢ·Aᵢ-₁ ··· A₁·A₀
ToWorld 행렬은 Aᵢ 부터 A₀까지의 모든 행렬을 다 곱한 행렬을 만들어야 한다.
그렇다면 루트 행렬을 구하려면?
마지막 A₀이 루트 좌표에서 월드 좌표로 변환하는 부분이므로,
ToWorldᵢ = Aᵢ·Aᵢ-₁ ··· A₁
A₀을 제외하고 나머지를 곱해주면 된다!
정리하면 ToWorldᵢ (i번째 관절의 좌표를 월드 좌표로 변환하기 위한 행렬)을 구하려면
ToWorldᵢ = Aᵢ * ToWorldᵢ-₁가 되며,
ToWorldᵢ = ToParentᵢ * ToWorldᵢ-₁가 된다.
(ToParentᵢ는 i번째 부모의 좌표로 이동하기 위한 행렬이기 때문)
T포즈란?
기본적으로 모델은 T포즈(Bind Position)으로 되어 있는데 T포즈는 배꼽이나 발밑을 기준으로 좌표가 형성되어 있고,
팔을 좌우로 쭉 뻗은 상태의 모델링을 의미한다.
예를 들어 F₂라는 관절이 45도 틀어져 있다고 할 때,
지금 상태에서는 그 배꼽이나 발밑을 기준으로 회전되어 있는 상태이다.
결국 F₂ 관절의 부모인 F₁ 기준으로 회전해야 하므로
이 발밑 기준 좌표를 관절 좌표 (Local Bone Position)으로 변환해주는걸 Offset 변환이라고 한다.
결국 최종적으로 화면에 그려지기 위해 어떤 정점에 대한 연산은 아래와 같이 이루어진다.
Finalᵢ = Offsetᵢ * ToRootᵢ
이 Finalᵢ 행렬을 이용하면 모델에 있는 좌표가 Root 기준 좌표로 변환되며,
이는 로컬좌표이므로 이 좌표에 월드행렬을 곱해주면 우리가 표현하고 싶은 방식대로 화면에 보여지게 된다.
(위에서 가중치에 대한 언급을 한 게 이 Finalᵢ 행렬)
애니메이션이 저장되는 방식
애니메이션이 움직이기 위해서는 각각의 관절이 프레임에 따라 어떻게 움직여야 하는지를
2차원 배열에 저장한다.
행은 각각의 프레임을 의미하며, 열은 Bone (관절)을 의미한다.
실제로 저장되는 값은 행렬일 수도 있고, SRT 변환 값일 수 있다.
Bone1 ~ Bone8을 예시로 들면,
Bone1 | Bone2 | Bone3 | Bone4 | Bone5 | Bone6 | Bone7 | Bone8 | |
Frame1 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame2 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame3 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame4 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame5 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame6 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame7 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
Frame8 | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat | FinalMat |
위와 같이 배열이 구성된다고 볼 수 있다.
'DirectX > [Inflearn_rookiss] Part2: DirectX12' 카테고리의 다른 글
35. Animation - 실습 (0) | 2023.02.23 |
---|---|
33. Mesh (0) | 2023.02.21 |
32. Picking (0) | 2023.02.20 |
31. Terrain (2) | 2023.02.20 |
30. Tessellation (0) | 2023.02.19 |
댓글