58. 애니메이션_애니메이션 이론
1. 뼈대에 관한 내용 - T포즈라는 첫 상태의 글로벌에서 Relative로 넘어갔다가 다시 애니메이션의 글로벌로 넘어가는 작업이 핵심
애니메이션을 할 때 15. ModelDemo.fx의
MeshOutput VS(VertexTextureNormalTangent input)
{
MeshOutput output;
output.position = mul(input.position, BoneTransforms[BoneIndex]);
output.position = mul(output.position, W);
output.worldPosition = output.position.xyz;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3)W);
output.tangent = mul(input.tangent, (float3x3)W);
return output;
}
이 부분이 중요한 대목이다.
Tank의 경우 mesh가 많았다.
mesh가 하나씩 노드(유니티에선 오브젝트)에 붙어있다.
노드가 계층 구조로 되어 있어서 계층 구조로 인식할 수 있었다.
Tank 안에서 자신을 기준으로 한 좌표에서 로컬(Root)좌표 변환을 하는게 핵심이었다.
유니티에서 사람 모양의 오브젝트를 보면 탱크처럼 여러 부품으로 되어 있는게 아니라 하나의 메쉬로 되어 있다.
뻐대에 메쉬가 붙어있지는 않다. 이거를 이용해서 애니메이션 작업을 해볼 것이다.
15. ModelDemo.fx의 BoneTransforms[BoneIndex]에 들어갈 데이터를 구해주는
Converter::ReadModelData에서
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// 2) Root(Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
bone->transform = bone->transform * matParent;
자신을 중심으로 한 좌표계에서 루트를 기준의 좌표계로 변환하는 matrix를 구하는 이 부분이 핵심이었다.
정점과 원점이라는 것을 구분해서 생각할 필요가 있다.
매쉬가 하나 있으면 어떻게 애니를 적용해서 움직일까?
2D 게임이면 여러개의 이미지를 틀어주면 애니가 된다.
3D의 경우는 여러개 틀기에는 한개의 용량이 크다.
정점에 뼈대를 박아서 움직이는 건 정점이 아니라 뼈대(관절)가 움직이는게 핵심이다.
보통은 허리가 최상위가 되고 계층구조가 있다.
T자형으로 인간이 있다 가정을 하면 여러 관절을 박아서 뼈대가 움직일 때 좌표계 변환을 어떻게 해줄 것이냐가 중요했다.
뼈대까지는 지난번에 했던 것의 연장선이다.
각 메쉬의 Relative를 기준으로 되어 있는 걸 행렬을 곱해서 최종 루트를 기반으로 하는 것으로 변환을 해서 빠져 나왔었다.
SRT를 곱할 때 마다 부모의 좌표계로 올라갈 수 있었다.
전역으로 빠져나온 건 코드에서 Global이라고 부르도록 할 것이다.
애니메이션을 적용 시킨다는 건 T자형으로 가만히 있는게 아니라 다른 모양을 취한다는 것이다.
바뀌는 부분은 관절의 상대 위치가 바뀌어서 모양이 바뀌는 것이다.
뼈대랑 정점을 구분해야 한다. 여기서는 정점이 아니라 관절을 얘기하고 있다. 만약에 Relative 좌표를 기준으로 발바닥을 기준으로 계산을 했을 때 Relative 에서 빠져 나와서 Global 좌표계로 변하고 싶다면 SRT를 곱해서 부모까지 기어 올랐다는 걸 알 수가 있다.
정점은 정점 정보를 받고, 메시에서 정점 정보를 파싱하는 부분이 어딘가에 들어있을 것이다.
Converter::ReadMeshData를 하면서
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex; // VertexTextureNormalTangentBlendData에 필요한 내용을 채워준다
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
여기서 정점 정보를 받고 있다.
이 정점은 하나의 기준으로 맺어져 있다.
여기서 받아준 정점들이 최종적인 글로벌 좌표라고 가정을 해본다.
각각의 정점의 좌표가 파일에서 받아준 이런 좌표들이랑 일치한다고 가정을 하고, 그랬을 때 여기서 뭔가 좌표변환을 통해서 해줘야 하는데 Global로 받은 좌표를 Realtive 좌표로 바꾸려면 어떻게 해야 할까? 발바닥의 위치가 0,1,3이 글로벌을 기준으로 하는 걸 다시 발바닥을 관절을 기준으로 하는 좌표계로 변환하고 싶다면 어떻게 해야할지 생각을 해볼 필요가 있다.
글로벌의 역행렬을 취하면 그거를 다시원래 있던 이 뼈대로 돌아가는 걸 구할 수 있다. 이걸 왔다 갔다 할 수 있어야 한다.
글로벌을 기준으로 할 것이냐, 관절을 기준으로 할 것이냐를 언제든지 좌표계 변환을 통해서 왔다 갔다 할 수 있어야 한다.
이 얘기가 왜 중요하냐면, 애니메이션을 파싱해서 하나의 애니메이션이 들어가게 될 것인데, 애니메이션이라는 건 결국 관절들의 움직임을 표현하는게 하나의 애니메이션 파일이라고 했다.
관절이 움직인다는 것은 상대적으로 어디에 위치해 있느냐가 계속 바뀌는 것이다.
해야 할 것은 T포즈라는 첫 상태의 글로벌에서 Relative로 넘어갔다가 다시 애니메이션의 글로벌로 넘어가는 작업을 해야 한다는 게 핵심이다.
이것을 왔다 갔다 하면서 관절의 위치에 따라 변환이 필요하다.
여기까지 뼈대에 대한 내용이었다.
2. 정점에 관한 내용 - 뼈대가 어떻게 움직이는지에 대한 정보만 있어도 알아서 나머지 정점들에 대한 좌표를 구해줄 수 있다는게 핵심
그 다음 중요한 부분이 정점에 대한 것이다.
정점같은 경우는 2D 애니메이션처럼 매 프레임마다 넣기는 용량이 너무 크다.
뼈대만 움직이는 건 좋은데 정점도 뼈대를 따라가서 움직여야 한다.
뼈에 고기가 붙어있는 느낌이라 생각하면 무릎 어딘가에 있는 정점이 있다 했을 때 얘는 어떠한 관절을 따라 갈 것인가를 정해주면 된다.
정점이 어디 소속인지를 정해줘야 된다는 얘기가 된다.
가슴근육 같은 경우는 허리를 움직일 때도 움직일 것이고, 여러개의 영향을 받아서 좌표가 움직이게 될 것이다. 이것의 비율을 정해주는데 최대 4가지 까지를 정해줄 수 있다.
가슴이랑 배꼽에서 영향을 6:4로 정해준다면, 퍼센티지로 정해줘서 두가지의 영향을 동시에 받는 걸로 계산을 해주면 실질적으로 애니메이션이 틀어져서 위치가 변환 했다고 했을 때 두가지의 6:4로 한 위치에 위치했다고 정해놨으니까 그걸 이용해서 최종 위치를 정해줄 수 있다.
정점에 대한 정보와 관절에 대한 정보를 조합을 해서 최종적인 포지션을 정해 주는게 애니메이션의 최종적인 목표라고 할 수 있다.
말로 하는 것도 어려운데 실제로 코드도 어렵다.
전체 과정중에 가장 난이도가 어렵다.
가장 중요한 건 이 Global→Relative→Global 이것에 대해서만 잘 기억하고 있다고 하면 그래도 코드를 보면서 따라가는 건 그나마 수월하다고 볼 수 있다.
물론 이렇게 포즈가 변한 것인데, 포즈가 변했다고 끝나는게 아니라 지속적으로 움직일 것이고, 30프레임이면 1초에 30번 모션이 바뀐다는 것이니 뼈대들이 움직이는 걸 파일로 만들것이고, 시간에 따라 어떤 정점이 어디에 있는 구해주는게 목표이다.
여기서 말하는 기법 중에서 정점이 관절에 붙어있다고 하는걸 스키닝이라고 하는데 결국에는 정점하나가 6:4 비율로 따라간다고 하면, 정해지는 순간은 메쉬에 따라 정해지는 것이고, 애니는 관절들이 어떻게 움직일지만 정해주게 될 것인데, 이 뼈대가 어떻게 움직이는지에 대한 정보만 있어도 알아서 나머지 정점들에 대한 좌표를 구해줄 수 있다는게 핵심이다.
이 작업을 해보면 될 거 같다.
이론은 여기까지 하고 코드로 돌아가서 해보자.