2015.08.06 22:37

원문 출처 : http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577


번역 및 개인 정리 (오역의 여지가 있으니 혹시나 이 글을 참고할 분들은 원문을 같이 살펴보는 것을 권장합니다 ㅜㅜ)


이 기사는 스키닝 매쉬 애니메이션의 방법을 설명한다.


게임은 애니메이션을 갖는 캐릭터를 자주 사용한다. 이를테면 걷기, 달리기, 사격등등...

이러한 애니메이션을 갖는 캐릭터에 자주 쓰이는 것이 스키닝 매쉬 애니메이션 기법(Skinned Character Animation)이다.

이런 스키닝 메쉬와 애니메이션 동작들은 몇몇 모델링 프로그램으로 만들어진다 ( 예를 들면 Blender)

이러한 모델들은 다양한 API를 통해 적절한 포멧으로 임포트/익스포트된다.

지금부터 이 기사는 데이터가 "임포트된 이후"의 애니메이션 과정에 대해 살펴보려한다.

모델링 과정, rigging, 애니메이션에 대해 언급할 것이고 임포트나 익스포팅 과정은 제외되니 알아두길 바란다.


Notes

------------------------

기사내 등장하는 SRT ​라는 용어는 변환과정의 Scale, Rotation, Translation 연산을 말한다.

대부분의 어플리케이션에서는 원하는 결과를 위해서는 SRT의 순서로 연산을 수행한다.


많은 API에서의 벡터-행렬 곱셈과 행렬-행렬 곱셈은 아래의 방법을 따르고 대표적으로 DIrectX가 다음과 같은 방법을 사용한다.

[ 역자 : 행행렬 또는 열행렬은 벡터를 표현하기 위해서 종종 사용되는데 어떤 것을 사용하는 것이 표준인지는 따로 정해진 것이 없다.

 다만 대부분의 수학책과 openGL에서는 주로 열벡터를 사용하고 DirectX 에서는 행 벡터를 사용한다. ]


// order of multiplication in DirectX

FinalVector = vector * ScaleMat * RotationMat * TranslationMat



OPENGL에서는 이런 곱셈 연산의 순서가 반대이다.

openGL 어플리케이션에서는 보통 "열 벡터" 행렬을 사용하기 때문이다.


// order of multiplication in OpenGL FinalVector = TranslationMat * RotationMat * ScaleMat * vector



* 수학적으로 위, 아래 식의 결과는 같다. openGL과 DirectX 모두 Scale, Rotation, Translation 순서로 연산된다.


스키닝 애니메이션은 이러한 SRT-변환이 행렬의 형태로 표현되는 것을 전제로 이루어진다.

행렬 곱의 특성에 의해 SRT는 하나의 행렬로 표현 가능하며 이것은 각각 SRT를 계산한 결과와 같다.

final-matrix = SRTmatrix1(rot1 followed by trans1) * SRTmatrix2( rot2 followed by trans2).



The Components of an Animated Skinned Mesh


"Animated"는 움직이거나 혹은 움직이는 것처럼 보이는 것을 뜻한다.
"Skinned"는 프레임 또는 뼈 계층구조의 매쉬를 뜻하는데 이 매쉬의 정점들은 다양한 위치에 그려질 수 있어 뛰거나 울렁이는 것처럼 나타낼 수 있다.

"Mesh"는 점들의 집합이며 그리기를 위한 면(삼각형, 사각형)을 생성하고 이는 그 위치에 제한받지 않는다(SRTs).


What's a "bone?"


"Frame"(프레임)이라는 단어가 사용되는데 이는 "Frame of Reference"(참조 프레임) 또는 월드에서의 좌표축의 방향을 의미한다.
[역자 : 참조 프레임이라는 것은 물리학/수학에서 방향을 가진 점을 표현하기 위하여 사용하는 좌표계이다. 이것은 상대적인 개념을 표현하기 위해 사용될 수 있다. ]
"bone"(본) 이라는 단어는 "frame"대신에 자주 사용되는데 왜냐하면 bone 주변의 스킨이 어떻게 움직이는지에 대한 개념으로 사용되기 때문이다.
만약 팔을 들어올리면 팔의 뼈는 주변의 피부가 위로 늘어나도록 영향을 줄 것이다.

여기서 "bone"이라는 단어는 뼈의 길이조차도 암시하지만 이 기사의 애니메이션에서 설명하는 "bone frame"은 뼈의 길이의 의미와는 관련이 없다.
뼈의 길이라는 것이 뼈에서 자식 뼈 사이의 거리( 예를들면 손뼈와 팔뼈는 13인치정도 떨어져있는 것처럼)에 대한 생각일지라도 이 스키닝 매쉬에서의 뼈는 하나 이상의 자식뼈를 갖고 자식 뼈는 부모로부터 같은 거리가 떨어져있어야 할 이유가 없다.
예를들면 인간의 캐릭터가 하나의 목 뼈를 갖고, 왼쪽 오른쪽 어께가 등 뼈의 자식이 되는 것은 이상하게 여겨지지 않는다.
자식 뼈는 관절 뼈로부터 같은 거리를 갖는 것이 필요하지 않다.
[ 위 문단이 잘 이해가 되지 않아 원문을 같이 올림]
 However, the term "bone" implies a length associated with it. Bone frames used in the type of animation described in this article do not have an associated length. Though the "length" of a bone might be thought of as the distance between a bone and one of its child bones (lower arm bone with a hand bone 13 inches away), a bone as used for skinning meshes may have more than one child bone, and those child bones need not be at the same distance from the parent. For instance, it is not unusual for a human character to have a neck bone, and right and left shoulder bones, all being children of a spine bone. Those child bones need not be at the same distance to the spine bone.

 

 


일반적인 프레임 계층구조에서 루트 프레임을 제외한 모든 프레임은 부모 프레임을 갖는다.

이것은 구조 전체가 단일 행렬만으로 비례확대, 회전, 평행이동을 모두 가능하게한다.

아래의 계층구조를 보자.

프레임이름의 바로 아래에서 프레임의 자식-부모 관계를 가리킨다.

eg) 엉덩이 프레임은 루트 프레임의 자식이고, 왼쪽 넓적다리는 엉덩이의 자식이다. 좀 더 살펴보면 등뼈는 엉덩이와 같은 레벨의 프레임인걸 알 수 있다.

이것은 엉덩이와 등뼈가 서로 형제관계이며 루트의 자식이라는 것을 보여준다.

만약 루트프레임에 SRT-변환이 적용되면 SRT-변환은 자식에서 자식으로 트리 전체에 전파될 것이다.

eg) 만약 루트가 회전하면 엉덩이는 루트에 의해 회전되고 왼쪽 넓적다리는 엉덩이에 의해 회전된다.

비슷하게 만약 루트에 평행이동변환이 적용되면 전체에 평행이동 변환이 적용될 것이다.


스키닝 애니메이션에서 SRT-행렬은 계층구조의 어떤 프레임이나 적용 가능한데 이것은 자식에서 그들의 자식들을 통해 오직 아래로만 전파된다.

부모에게는 아무런 영향을 주지 않는데 만약 Left Upper Arm을 위쪽으로 회전시키면 Left Lower Arm과 Left Hand만이 같이 위로 회전하고

Right Clavicle이나 Spine, root에는 영향을 주지 않기 때문에 우리가 원하는 대로의 움직임이 보여질 것이다.



Root frame 

Hip

Left Thigh

Left Shin

Left Foot

Right Thigh

Right Shin

Right Foot

Spine

Neck

Head

Left Clavicle

Left Upper Arm

Left Lower Arm

Left Hand

Right Clavicle

Right Upper Arm

Right Lower Arm

Right Hand


이러한 계층 구조는 크레인이나 의자, 탱크와 같은 물체에서도 볼 수 있지만 스키닝 애니메이션의 진정한 장점은 이런 물체들이 아닌 피부를 가진 사람이나 동물의 매쉬에서

적절한 가중치를 가진 뼈를 움직일 때 피부가 자연스럽게 늘어나며 마치 비선형적인 움직임처럼 보이게 하는 것이다.

예를들면 팔을 들 때 가슴과 어께 사이의 피부가 쭉 늘어나는 것.


이 기사에서 논의하는 프레임(뼈)는 행렬들과 스키닝 매쉬 애니메이션으로 구현된다.

각각의 프레임들은 애니메이션을 수행하는 동안 싱글 랜더 사이클에 관련된 몇몇 행렬들을 갖는다.

각각의 뼈들은 예를들면 bone의 To-root SRT행렬과 관련있는 행렬인 "offset matrix"를 갖는다. 

각각의 뼈들은 "key matrix"를 갖고 : 요건 애니메이션 동안에 각각의 뼈의 To-Parent 변환 행렬임

또 "animation matrix"와 "final matrix"를 갖는다.


지금 위에서 설명한 것이 꽤나 복잡해보이지만 하나씩 차근차근 접근하면 이해할 수 있다.



The Frame Structure


계속해서 진행하기 전에 "Frame"이 어떻게 실제 코드상에서 표현되는지 예제를 살펴보자.
슈도코드는 C,C++ 처럼 보이지만 이 글을 읽는 프로그래머라면 자신에게 맞는 언어로 표현할 수 있으리라 생각한다.
struct Frame { string Name; // the frame or "bone" name Matrix TransformationMatrix; // to be used for local animation matrix MeshContainer MeshData; // perhaps only one or two frames will have mesh data FrameArray Children; // pointers or references to each child frame of this frame Matrix ToParent; // the local transform from bone-space to bone's parent-space Matrix ToRoot; // from bone-space to root-frame space };

보통 각각의 프레임은 이름을 갖는다. (위의 계층구조 예에서는 root, hip, spine, etc ... ) 이 이름은 계층 구조상의 이름과 같을 것이다.

다른 프레임의 구조체 멤버의 용도에 대해서는 좀 더 나중에 자세하게 설명할 예정이다.


Animation Data and How It's Used


아래에 보여지는 데이터는 스키닝 애니메이션 메쉬에 필요하다.
물론 이것이 프레임 계층 구조에서 사용되는 모든 데이터는 아니다.

일반적으로 프레임 계층구조는 

 - 뼈의 구조에 대한 설명,

 - 매쉬 구조체,

 - 그리고 매쉬의 정점들과 뼈들간의 관계를 나타내는 데이터를 갖는다.

 이러한 모든 데이터가 스키닝 매쉬에서의 "rest pose"를 나타낸다.

[ 역자 : "rest pose"는 "T pose" 또는 "reference pose"등으로도 불려지는데 그냥 "기본포즈"라고 부르겠다. ]


애니메이션 데이터는 흔히 별도로 저장되고 접근된다.

이것은 매쉬의 단일 액션 표현을 위해서인데 예를들면 "걷기", "뛰기" 같은거...

이러한 동작은 아마도 하나 이상 설정될테지만 하나의 계층구조 위에서 일어난다는 점을 기억하자.


스키닝 애니메이션에 필요한 전체 데이터는 다음과 같이 구성된다.


 * 기본 포즈에서의 매쉬 (아마 프레임이 포함될 것임)

 * 프레임 계층구조. 계층구조는 다음으로 구성됨

 - 루트 프레임

 - (모든 프레임마다) 자식 프레임을 가리킬 리스트나 포인터 배열

 - (모든 프레임마다) 프레임이 매쉬를 갖는다면 그 매쉬에 대한 포인터

 - (모든 프레임마다) SRT-변환을 위한 하나 혹은 그 이상의 행렬들

 * 뼈 데이터에 영향을 주는 다양한 배열들

- 각각 뼈 데이터에 영향을 미치는 오프셋 행렬

- 각각 뼈 데이터에 영향을 미치는 정점의 인덱스들과 가중치들

* 애니매이션들의 배열, 각각의 애니메이션은 다음으로 구성됨

- 각각 애니메이션이 적용되는 뼈에대한 포인터

- "Key"들의 배열, 각각의 key는 다음으로 구성됨 (시간+프레임)

- 애니메이션의 시간을 가리킬 tick 카운트

- 각 tick 카운트에 적용될 행렬( 또는 각각의 SRT-변환들)


 * ticks-per-second (초당 몇 tick이 진행되어야 하는지에 대한 데이터)


Note : 모든 데이타 파일에서 위의 정보를 항상 갖는 것은 아니다. 예를 들어 어떤 데이터 포멧에서는 오프셋 행렬을 갖지 않을 수도 있는데 이렇게되면 직접 만들어야한다.

오프셋 행렬에 대해서는 아래에서 설명하겠다.


The Mesh


매쉬 데이터는 점들의 배열, 루트 프레임에 대한 상대적인 위치, 그리고 노말벡터나 텍스처 UV, 기타 등등으로 구성된다.

이 때 정점의 위치는 보통 기본 자세에서의 위치를 뜻한다 ( 애니메이션이 진행되는 동안의 위치가 아님!)

만약 매쉬 데이터가 루트 프레임보다 자식 프레임이면 스키닝 과정에서 반드시 그것에 대한 설명이 필요한데 이는 아래에 설명될 오프셋 행렬에서 논의될 것이다.


애니메이션이 적용된 매쉬는 셰이더나 이펙트 안에서 가장 많이 랜더링된다.

이러한 셰이더는 랜더링하는 동안 정점 입력으로 특별한 순서를 기대하는데데이터를 임포트하여 호환되는 포멧으로 바꾸는 과정은 이 기사에서 다루지 않는다.


Initializing The Data


스키닝 매쉬 애니메이션을 위해 필요한 데이터, 애니메이션을 가능하게 하는 과정의 개요가 여기 있다.

요구되는 많은 데이터들은 외부 파일에서 로드하게 된다.

그 후에 프레임 계층 구조를 생성하고, 프레임 구조체를 참조하여 최소 Name, Mesh Data, children, To-parent matrix 을 채워야한다.


각각의 프레임을 위한 To-root 행렬은 다음과 같이 초기화한다.

// given this function ...
function CalcToRootMatrix( Frame frame, Matrix parentMatrix )
{
    // transform from frame-space to root-frame-space through the parent's ToRoot matrix
    frame.ToRoot = frame.ToParent * parentMatrix;

    for each Child in frame: CalcToRootMatrix( Child, frame.ToRoot );
}

// ... calculate all the Frame ToRoot matrices 

CalcToRootMatrix( RootFrame, IdentityMatrix ); // the root frame has no parent 


위 함수와 같은 재귀함수는 처음에 이해하는 것이 조금 어려울 테지만 다음의 과정을 보면 무슨 일이 일어나는지 좀 더 이해가 될 것이다.


frame.ToRoot = frame.ToParent * frame-parent.ToParent * frame-parent-parent.ToParent * ... * RootFrame.ToRoot


애니메이션을 위한 몇몇 데이터는 오직 매쉬 정점에 영향을 미치는 뼈들에게만 영향을 미칠 것이다.

따라서 랜더링 과정중에 데이터는 뼈의 이름이 아닌 뼈의 인덱스에 의해 접근한다.

뼈가 영향을 주는 데이터의 배열을 사용하는 스킨에대한 정보 객체는 뼈 정보를 인덱스 배열로 제공하고 또한 그 인덱스를 통해 뼈의 이름도 얻을 수 있다.


또한 초기화 과정에서 오프셋 행렬들의 배열을 생성하는데 이 행렬은 뼈의 각각의 영향을 위한 것이고, 만약 모든 계층구조 프레임이 뼈에 영향을 주지 않는다고 하면

이 행렬은 프레임들의 수보다 적을 것이다.

 

A Slight Diversion from the Initialization Process


이건 초기화 과정은 아니지만 "프레임의 변환 행렬(Frame TransformationMatrix)"에 대해 이해하는 것은 왜 애니메이션에서 오프셋 행렬들이 필요한가에 대해 이해하는 것에 도움을 줄 것이다.

프레임의 변환 행렬은 애니메이션을 사용하는 각각의 랜더링 사이클마다 채워진다.

애니메이션 관리 객체 또는 함수는 이 변환을 계산하기 위해 애니메이션 데이터를 사용하고 그 계산된 결과를 프레임 변환 행렬에 저장한다.

이 행렬은 정점들을 "bone's reference frame"에서 "bone's parent's animated reference frame"으로 변환한다.

이것은 To-parent 행렬과 비슷한데 이것은 "pose position"으로의 적용이다.

이 행렬은 "animated position"에 적용된다.(애니메이션동안에 매쉬가 어떻게 보이게하는지)

이 행렬에 대해 할 수 있는 생각중 하나는 [어떻게 뼈가 "pose position"에서 "animated position"으로 바꾸는지이다.] 


아래의 상황에 대해 생각해보자.

애니메이션이 일어나는 동안 캐릭터의 손이 뼈의 영향을 받아 살짝 회전한다.

 

Bone-frame Rotation



만약 bone의 변환행렬이 영향을 미치는 모든 정점에 대해서 적용된다고 하면, 정점은 root-frame 공간으로 변환되고, root-frame 공간에서 회전하기 때문에 원하지 않은 결과를 보이게 될 것이다.

 


따라서 아직 올바른 변환을 위해서는 정점을 bone-space로 변환시킬 행렬이 필요한데...


Back into Initialization - The Offset Matrix


Note:  오프셋 행렬은 이미 많은 파일 포멧에서 제공되기 때문에 뼈의 영향에 따른 오프셋 행렬의 정확한 계산은 필요하지 않을 수 있다.

하지만 오프셋 행렬을 이해하기 위한 목적으로 정보를 제공한다.


애니메이션이 일어나는 동안에 뼈에 대한 영향을 고려하는 점에 대한 적절한 변환에서 

반드시 root-frame에서의 "pose"  position이 bone에서의 pose frame으로 변환되어야한다.

일단 한 번 변환되면 bone의 변환행렬은 위에서 bone-frame Rotation 그림에서처럼

"어떻게 pose position에서 animated position으로 정점을 바꿀지"에 대한 결과를 만드는 적용을 할 수 있게된다.


정점(root-frame에서의)을 bone-space에서의 "pose" position으로 변환하는 이 행렬은 오프셋 행렬(offset matrix)이라고 불린다.

만약 매쉬가 루트-프레임이 아닌 경우, 매쉬가 부모 프레임에 있다면, 이전 단락에서 언급했던 root-frame에서의 "pose" position으로의 변환 이전에

이 부모-프레임은 루트-프레임 공간으로 변환되어야한다.

운좋게도 이것은 매쉬의 부모-프레임의 To-Root 행렬을 사용하므로써 쉽게 이루어진다.


각각의 영향을 주는 뼈 프레임은 To-Root행렬을 가지고 있다.

하지만 이것은 bone-space에서 root-frame-space로의 변환이다.

따라서 역변환이 필요하다.

행렬 수학에서 역행렬이라는 이 간단한 개념을 통해 정점에 어떤 변환을 곱하든지 상관없이 그것을 그 반대로 만들어 버릴 수 있다.


오프셋-행렬들은 다음과 같이 계산된다.

// A function to search the hierarchy for a frame named "frameName" and return a reference to that frame
Frame FindFrame( Frame frame, string frameName )
{
    Frame tmpFrame;

    if ( frame.Name == frameName ) return frame;
    for each Child in frame {
        if ( (tmpFrame = FindFrame( Child, frameName )) != NULL ) return tmpFrame;
    }
    return NULL;
}

// Note: MeshFrame.ToRoot is the transform for moving the mesh into root-frame space.
function CalculateOffsetMatrix( Index boneIndex )
{
    string boneName = SkinInfo.GetBoneName( boneIndex );
    Frame boneFrame = FindFrame( root_frame, boneName );
    // error check for boneFrame == NULL if desired
    offsetMatrix[ boneIndex ] = MeshFrame.ToRoot * MatrixInverse( boneFrame.ToRoot );
}

// generate all the offset matrices
for( int i = 0; i < SkinInfo.NumBones(); i++ ) CalculateOffsetMatrix( i );



A pseudo-expansion of an offset matrix is as follows:
 

offsetMatrix = MeshFrame.ToRoot * Inverse( bone.ToParent * parent.ToParent * ... * root.ToParent )


A further pseudo-expansion including the Inverse would be:
 

offsetMatrix = MeshFrame.ToRoot * root.ToSomeChild * Child.ToAnotherChild * ... * boneParent.ToInfluenceBone


이 오프셋 행렬은 "pose" position 데이터로부터 단 한 번 계산되지만 이 계산은 계속되는 랜더 사이클 내내 사용된다.

  



The Root Frame


"root"는 계층 구조에서 다른 모든 뼈들의 부모가 되는 프레임이다.

모든 뼈는 부모 또는 부모의 부모를 거슬러 올라감으로써  root를 부모로 갖는다.


만약 루트-프레임에 SRT변환이 적용되면 모든 계층구조 프레임에 SRT변환이 적용된다.

루트-프레임은 매쉬에서 어느 곳이나 위치할 수 있지만 애니메이션 캐릭터 매쉬에서 캐릭터의 중앙에 위치하는 것이 편리하다.

예를 들어 땅 위에서 움직이는 캐릭터의 경우 발과 발 사이에 위치하면 좋을 것이고, 만약 날아다니는 캐릭터라면 캐릭터의 무게중심에 위치하면 편리할 것이다.

이러한 사항은 모델링 과정에서 결정된다.

루트-프레임은 가장 최상단 프레임이기 때문에 부모를 갖지 않고 오직 자식만이 존재한다.


A Bone and It's Children


몇몇의 bone들은 그들이 영향("influence")을 주는 정점들과 연관되어있다.

"influence"의 의미는 bone이 움직일 떄 정점 또한 같이 움직인다는 것을 의미한다.

예를 들어 팔의 bone은 팔꿈치와 손목 부분의 매쉬에 영향을 줄 것이다.

만약 팔을 회전시키면 이 팔이 영향을 주는 점들도 같이 회전의 영향을 받는다.

또한 팔의 움직임은 손목 -> 손가락으로 이어지며 움직이게 만든다.

그렇게 되면 손목과 손가락 bone에 영향을 받는 매쉬의 정점들 또한 영향을 받아 움직인다.

하지만 팔보다 위에 있는 위쪽 팔 또는 다른 부위는 영향을 받지 않는다.


이러한 것들은 어떤 프레임이 움직일 때 이 프레임의 움직임은 아무 프레임에게나 영향을 주는 것이 아니라 영향을 주는 프레임이 정해져 있다는 것을 의미한다.

이러한 프레임들은 여전히 "bone" 이라고 불리지만 "influence bone"이라고 불리지는 않는다.

이러한 프레임들의 동작은 여전히 애니메이션이 일어나는 동안 자식쪽을 향하고, 변환 행렬의 계산은 여전히 계층 구조를 따른다.

 


The mesh and bone hierarchy in pose position 


Bone Influences and Bone Weights


이 데이터는 bone이 영향을 미치는 정점의 배열 + 가중치이다.

각각의 bone에 대하여 몇몇의 쌍을 이루는 데이터인데, 정점의 인덱스 번호와 0~1사이의 float 값 이라고 할 수 있다.

정점의 인덱스는 매쉬 정점 배열에서의 위치를 나타내는 인덱스를 말하고, 가중치라고 불리우는 0~1사이의 실수는 bone이 움직일 떄 거기에 영향을받는 

정점의 위치가 얼마나 많이 움직일지에 대한 상수이다.

만약 하나의 정점에 대해서 여러개의 bone이 영향을 미친다면 이 각각의 bone의 가중치의 합은 항상 1이된다.

예를 들어 하나의 정점에 두 개의 bone이 영향을 미치는데 하나의 가중치가 0.3이라면 다른 하나의 bone이 갖는 가중치는 자연스럽게 0.7이라는 것을 유추할 수 있다.


이러한 데이터는 주로 SkinInfo 객체에서 갖고 있다.

SkinInfo 객체가 어떻게 이러한 데이터를 다루는지에 대해 설명하는 것은 이 기사의 범위를 벗어나므로 설명하지 않겠다.


The Animation Process


"pose" position에서의 계층 구조에 대한 설명을 위에서 다루었었다.

이 매쉬를 실시간으로 애니메이션하기 위한 정보는 보통 계층구조 데이터들과 분리되어 존재한다.

이것은 의도적인데, 캐릭터의 단일 액션을 표현하기 위한 애니메이션 데이터( 걷기, 뛰기등등) 는 "pose" position에 적용될 수 있고,

이것은 캐릭터가 달리거나 걷는 상태에서 다른 액션으로 바뀔 수 있게 한다.

예를 들면 뛰면서 사격 모션을 취하기.


이 애니메이션 데이터는 보통 단순한 함수의 집합에 불과한 AnimationController  객체에서 저장되고 다루어진다.

역시 이 AnimationController 객체에 대해서 자세히 설명하는 것은 이 기사의 범위를 넘는 것이므로 생략하지만

그래도 AnimationController 객체가 하는 몇몇 기능은 아래에서 설명했다.


단일 캐릭터 액션을 위한 애니메이션 데이터는 "animation set"으로 불려지고 이것은 보통 frame-animation의 배열로 구성된다.

아래의 슈도 코드는 이해를 돕기위하여 첨부하였는데 아마 대부분의 animation set은 다음과 같은 형태로 이루어져 있을 것이다.


struct AnimationSet {
    string animSetName; // for multiple sets, allows selection of actions
    AnimationArray animations;
}

struct Animation {
    string frameName; // look familiar?
    AnimationKeysArray keyFrames;
}

struct AnimationKey {
    TimeCode keyTime;
    Vector Scale, Translation;
    Quaternion Rotation;
}


Animation Keys


각각의 프레임은 애니메이션 키의 집합과 연관되어있다.

이 애니메이션 키는 To-parent 변환과 애니메이션 과정중의 특정한 시간의 쌍으로 정의된다.

이 "시간"은 아마 정수형 타입으로 표현될텐데 count of ticks를 의미한다.( 애니메이션의 시작 시간을 표현하기 위해 실수형 타입이 추가되기도 한다. )

그러면 보통 최소 2개의 "time key"를 갖는데 하나는 애니메이션의 시작을 나타내기 위한 것이고 또 다른 하나는 애니메이션의 종료를 나타내기 위한 것이다.

여기서는 여러개의 키가 들어갈 수도 있다.

예를 들면 애니메이션의 틱 카운트 구간이 100이라고 가정할 때 0에서 팔을 들기 시작하고 50에서 팔이 모두 올라갔다가 내려오기 시작하고 100에서 다시 원상복귀되는 식으로 말이다.


키는 변환과 시간의 쌍으로 정의된다고 헀는데 여기서 변환을 표현하기위해 행렬이 사용되기도 하지만

개별적인 벡터의 집합으로 표현되기도 한다. ( 이동을 위한 벡터, 비례확대를 위한 벡터, 회전을 위한 쿼터니온 벡터 )

이렇게 벡터의 집합으로 표현하는 것은 보간(Interpolation)과정을 쉽게 하기 위해서이다.

아까 카운트가 100인 애니메이션의 예로 설명하자면 카운트가 25일때의 행동을 위해서는 0일때의 행동과 50일때의 행동을 보간하여 계산할 수 있다.

만약 키가 이렇게 벡터와 쿼터니온의 형태로 저장된다면 변환행렬은 이전 키와 다음 키의 translation, scaling, rotation)에 대한 보간으로 계산된다. ( 0과 50을 보간하여 25 카운트에서의 위치를 계산했던 예처럼 )


이러한 보간은 보통 다음과 같은 방법으로 계산된다 :

 NLERP (Normalized Linear IntERPolation) or SLERP (Spherical Linear intERPolation) of the quaternions and LERP (Linear intERPolation) of the vectors.


연속적인 틱에서 회전의 변화는 그렇게 크지 않기 때문에 대부분 NLERP를 사용하는 것이 결과도 꽤나 만족스러우며 빠르다.

만약 키가 행렬의 형태로 저장된다면 행렬에서 벡터부분과 쿼터니온 부분을 분리하여 보간 연산을 처리해야하는데 만약 scaling 변환이 적용된 경우 잘못된 결과를 산출할 수 있다.


프레임을 위한 행렬이 계산되면(애니메이션 과정에서 특정한 카운트에서 사용될) 

이 행렬은 프레임의 프레임 구조체에서 변환행렬로써 저장된다. ( frame's Frame structure as theTranformationMatrix.)


아까 언급했듯이 애니메이션 데이터는 프레임 계층구조와는 개별적으로 저장된다.

따라서 각각의 프레임 행렬을 알맞은 장소에 저장하는 것은 아래 예제에서의 FindFunction()과 같은 함수를 통해 이루어진다.

function CalulateTransformationMatrices( TimeCode deltaTime ) { TimeCode keyFrameTime = startTime + deltaTime; for each animation in AnimationSet: { Matrix frameTransform = CalculateFromAnimationKeys( keyFrameTime, animation.frameName ); Frame frame = FindFrame( rootFrame, animation.frameName ); frame.TransformationMatrix = frameTransform; } }




Ticks Per Second


스키닝 매쉬는 유저를 표현하기 위해 실시간으로 표현된다.

여기서 "실시간"은 초단위이지 틱 단위까지는 아니다.

만약에 100 tick count 애니메이션이 3초 걸린다면, 초당 틱 카운트는 100/3 tick가 될 것이다.

애니메이션을 시작하기 위해 처음 tick count는 0으로 설정되어야한다.

씬을 랜더하면서 마지막 랜더로부터 지난 delta time은 여기에 계속 더해져 증가해야한다.

만약 순환되는 애니메이션인 경우, 예를들면 count가 100까지인 애니메이션.

계속 틱 카운트를 더하다가 100이 넘어 120이 됬다고 치면 100을 빼서 20으로 만들고 계속해서 순환되게 해야할 것이다.



Yet More Matrices Must Be Calculated


하... 정말? 진짜?

그렇다. 아직 계산되야할 행렬이 더 남았다 ㅜㅜ

정점을 frame-space로 변환시키는 offset 행렬이 아까 언급되었었다.

그 부분에서 프레임의 TransformationMatrix  가 frame-space에서 적용되어야 했었다.

이 작업은 이제 정점을 frame-animated-space에서 root-frame-animated-space로 변환하고, 그러면 이제 랜더링 될 것이다.

이 변환을 계산하는 것은 "pose" frame-space에서 "pose" root-space로 변환하는 CalcToRootMatrix  함수와 비슷하다.

아래는 frame-animated-space를 root-frame-animated-space로 변환하는 계산이다.

모든 프레임에 root-frame-animated-space 변환에 대한 배열을 생성하여 갖는 것 보다는 TransformationMatrix  를 사용하여 간단하게

frame-space에서 root-space로 변환하자. 


// given this function ...
function CalcCombinedMatrix( Frame frame, Matrix parentMatrix )
{
    // transform from frame-space to root-frame-space through the parent's ToRoot matrix
    frame.TransformationMatrix = frame.TransformationMatrix * parentMatrix;

    for each Child in frame: CalcCombinedMatrix( Child, frame.TransformationMatrix );
}

// ... calculate all the Frame to-root animation matrices
CalcCombinedMatrix( RootFrame, IdentityMatrix );



Are We There Yet?


이제 거의 다왔다.

하지만 아직 빠진 정보가 있다.


정점의 위치는 offset 행렬을 통해 frame-pose-space로 변환되어야 하고,

정점의 위치는 TransformationMatrix를 이용하여 frame-animated-space에서 root-animated-space로 변환되어야 한다.

셰이더 프로그램 또는 랜더링 루틴에서 우리는 위 두개의 연산을 위한 행렬의 배열이 필요하다.

하지만 정말 필요한 것은 영향을 주는 bone의 행렬 뿐이다.


자 이제 FinalMatrix 로 불리우는 또다른 행렬들의 배열, SkinInfo.NumBones() 의 사이즈에 딱 맞는 이것이 생성되었다.

하지만 이 행렬은 이 행렬이 사용되는 매 랜더 사이클마다 다시 생성되어야 한다.

이 FinalMatrix의 계산은 아래와 같다.

// Given a FinalMatrix array..
function CalculateFinalMatrix( int boneIndex )
{
    string boneName = SkinInfo.GetBoneName( boneIndex );
    Frame boneFrame = FindFrame( root_frame, boneName );
    // error check for boneFrame == NULL if desired
    FinalMatrix[ boneIndex ] = OffsetMatrix[ boneIndex ] * boneFrame.TransformationMatrix;
}

// generate all the final matrices
for( int i = 0; i < SkinInfo.NumBones(); i++ ) CalculateFinalMatrix( i );


How It All Works Together


드디어 우리는 스키닝 애니메이션을 랜더링하기 위한 준비가 다 되었다.

매 랜더 사이클마다 아래의 작업을 수행하자.


1. The animation "time" is incremented. That delta-time is converted to a tick count. 


2. For each frame, a timed-key-matrix is calculated from the frame's keys. If the tick count is "between" two keys, the matrix calculated is an interpolation of the key with the next lowest tick count, and the key with the next higher tick count. Those matrices are stored in a key array.


3. When all the frame hierarchy timed-key-matrices have been calculated, the timed-key-matrix for each frame is combined with the timed-key-matrix for its parent.


4. Final transforms are calculated and stored in an array. 


5. The next operation is commonly performed in a vertex shader as GPU hardware is more efficient at performing the required calculations, though it can be done in system memory by the application.


The shader is initialized by copying the FinalMatrix array to the GPU, as well as other needed data such as world, view and projection transforms, lighting positions and paramaters, texture data, etc.


Each mesh vertex is multiplied by the FinalMatrix of a bone that influences that vertex, then by the bone's weight. The results of those calculations are summed, resulting in a weight-blended position. If the vertex has an associated normal vector, a similar calculation and summation is done. The weighted-position (see below) is then multiplied by world, view and projection matrices to convert it from root-frame space to world space to homogeneous clip space.

As mentioned above, proper rendering requires that the sum of the blend weights (weights for the influence bones) for a vertex sum to 1. The only way to enforce that assumption is to ensure that the model is created correctly before it is imported into the animation application. However, a simple bit of code can help and reduces by 1 the number of bone weights that must be passed to the vertex position calculations.

Calculating A Weighted Position
 

// numInfluenceBones is the number of bones which influence the vertex
// Depending on the vertex structure passed to the shader, it may passed in the vertex structure
// or be set as a shader constant
float fLastWeight = 1;
float fWeight;
vector vertexPos( 0 ); // start empty
for (int i=0; i < numInfluenceBones-1; i++) // N.B., the last boneweight is not need!
{
   fWeight = boneWeight[ i ];
   vertexPos += inputVertexPos * final_transform[ i ] * fWeight;
   fLastWeight -= fWeight;
}
vertexPos += inputVertexPos * final_transform [ numInfluenceBones - 1 ] * fLastWeight;


Summary


Data for a skinned mesh is loaded into or calculated by an application. That data is comprised of:
- mesh vertex data. For each vertex: positions relative to a frame of the bone hierarchy
- frame hierarchy data. For each frame: the frame's children, offset matrix, animation key frame data
- bone influence data - usually in the form of an array for each bone listing the index and weight for each vertex the bone influences.
 

Note:  Many of the operations described above can be combined into fewer steps and otherwise simplified. The intent of this article is to provide descriptions of the processes involved in animating skinned meshes using matrices, not necessarily in an efficient fashion.


신고
Posted by 우엉 여왕님!! ghostkyow

티스토리 툴바