IT 이모저모

소프트웨어 스키닝

exien 2018. 3. 5. 15:26

이글은 실시간 스켈레톤 애니메이션을 구현하는 여러가지 방법을 어떻게 자신만의 스켈레톤 애니메이션 시스템을 구현 하는지에 대해 설명한다. 최종 목표는 3D MAX,Chater Studio에의해 추출된 데이터를 가지고 실시간으로 애니메이션 시키는 것이다. 애니메이션 데이터는 캐릭터의 캐릭터의 애니메이션 키 position, rotation, scale값으로 구성된다. 스킨이 어떤 본에의해 얼마나 영향을 받는지에대한 스키닝 정보도 포함 된다.(만약 vertex가 하나의 본에만 영향을 받으면 weight 는 1이된다.)

BlendWeight 구조체를 다음과 같이 정의 할 수 있다.

 

struct BlendWeight

{

int iVertexIndex;   // blend vertex ID

float fWeight;       // 해당 vertex가 본에의해 얼마나 영향을 받는가?

};

 

3D studio max에서는 애니메이션의 처음 키는 위치와 연관된 상대적인것으로 주어진다. 그래서 애니메이션을 재생시킬때 우리는 TM을 위해서 'base' 위치를 사용한다. 그래서 우리는 이 매트릭스를 본에 포함시킨다. 아래는 본의 정의이다.

 

struct Bone

{

Bone();

Bone(const char* Name);

 

//본에 의해 영향 받는 vertex를 추가

void AddWeightedVertex(float weight, int blendVertexId);

 

//본의 자식본을 추가

void AddBone(int boneIndex);

 

//본의 이름을 반환

const char* GetName() const;

 

//본의 자식인지 체크

bool IsChild(int boneIndex);

 

//본의 부모를 반환

Bone* GetParent();

 

//본의 부모를 셋

void SetParentBone(int parentIndex);

 

//현재 본의 TM을 갱신

//(자식 본들 또한 갱신)

//[IN] dt - delta teim

//[IN] animController - 현재 본 갱신

//애니메이션 track

void Transform(float dt, AnimController *animController);

 

string m_sName;     //본이름

Matrix m_init;          //초기 pose

Matrix m_offset;      //local TM

//본의 최종 매트릭스(부모 변환까지 포함)

Matrix m_final;

int parentIndex;     //본의 부모

std::vector<int> children;  // 자식 본

std::vector<BlendWeight> m_blendVerts;

};

 

본의 3개의 매트릭스를 가진다. m_init 매트릭스는 본의 초기 TM 이다. 이것은 모델에 주어진 월드상의본의 위치이다. m_offset매트릭스는 기본적으로 이전 본의 오프셋 과 새로운 위치 사이의 차이 이다.

예를들어 본이 있는데 키 0가 위치(25,4,5)를 가지고 키 2가 위치(28,9,7)을 가진다고 해보자. 이때 오프셋 매트릭스의 값은 위치(3,5,2)가된다. 본의 최종 위치는 부모본의 TM에 오프셋 메트릭스를 곱하여 현재 프레임의 최종 TM(m_final)이 구해지게 된다. 따라서 우리는 계층 애니메이션을 구현할때 부모 본의 매트릭스부터 계산한뒤 자식본의 매트릭스를 계산한다.

 

//root의 world location의 위치를 임의적으로 해도 되고, 아니면 단위행렬로 해도 됨

Root Bone(Pelvis)

m_final.MakeIdentity();

//이전 프레임과의 오프셋 매트릭스 * 부모 location

Spine 03

m_final = m_offset * rootBone->m_final;

Spine 02

m_final = m_offset * spine03->m_final;

.....

 

다음은 본등에서 사용되는 매트릭스의 정의이다.

 

struct Matrix

{

union

{

struct

{

float _11, _12,_13, _14;

float _21, _22, _23, _24;

float _31, _32, _33, _34;

float _41, _42, _43, _44;

};

float m[4][4];

float mafEntry[16];

};

 

//default로 단위 행렬 생성

//인자의 scale로 scale시킴

Matrix(float scale=1)

: _11(scale), _12(0), _13(0), _14(0),

  _21(0), _22(scale), _23(0), _24(0),

  _31(0), _32(0), _33(scale), _34(0),

  _41(0), _42(0), _43(0), _44(1)

{

}

 

//매트릭스로 회전 시킨 벡터를 구함

inline void InverseRotateVect(float *pVect) const

{

float vec[3];

 

vec[0] = pVect[0]*_11 + pVect[1]*_12 _ pVec[2]*_13;

vec[1] = pVect[0]*_21 + pVect[1]*_22 _ pVec[2]*_23;

vec[2] = pVect[0]*_31 + pVect[1]*_22 _ pVec[2]*_33;

 

memcpy(pVect, vec, sizeof(float)*3);

}

 

inline void InverseTeanslateVect(float *pVect) const

{

pVect[0] -= _41;

pVect[1] -=_42;

pVect[2] -=_43;

}

 

void MakeIdentity()

{

Matrix tm;

memcpy(&m, &tm.m, sizeof(float) * 16);

}

 

//4*3 mul

void Mul(float* v) const

{

float x0 = v[0], y0 = v[1], z0 = v[2];

 

v[0] = x0 * m[0][0] + y0 * m[1][0] + z0 * m[2][0] + m[3][0];

v[1] = x0 * m[0][1] + y0 * m[1][1] + z0 * m[2][1] + m[3][1];

v[2] = x0 * m[0][2] + y0 * m[1][2] + z0 * m[2][2] + m[3][2];

}

 

void PreTranlate(const float* v)

{

_41 = v[0];

_42 = v[1];

_43 = v[2];

}

 

Matix Matrix::operator * (const Matrix& other) const

{

Matrix newMatrix;

int column = 0;

for (int i = 0; i < 4; i++) // rows

{

for (int j = 0; j < 4; j++) // each column

{

float val = 0;

for(int k = 0; k < 4; k++)

{

val += m[i][k] * other.m[k][j];

}

 

newMatrix.m[i][j] = val;

}

}

return newMatrix;

}

};

 

//key frame data

struct AnimKey

{

Vector pos; 

Quat4f rot; 

Vector scale;

int time; 

 

AnimKey()

: time(0)

{}

};

 

키프레임 데이터는 샘플링 되서 나오기 때문에 두개의 키사이의 값은 보간을 해야한다.

 

void AnimationInstanance::Update(AnimController *animControl, Bone* bone)

{

AnimSequence* currentSequence = 0;

int index = animControl->GetSequence();

 

assert(index it; sequences.size());

currentSequence = &sequences[index];

 

if(currentSequence == NULL)

{

if(sequences.size()>0)

{

//디폴트 pose

currentSequence = &sequences[0];

}

else

{

return;

}

}

 

//현재 시간을 구함

float time = animControl->GetTime();

 

if(time > currentSequence->GetMaxTime())

{

//애니메이션 시간 초기화

time = 0.0f;

}

 

animControl->SetTime(time);

 

//시작과 마지막 키를 찾음

AnimKey start, end, key;

 

//TODO: 본에 영향받는 track을 찾음

AnimTrack* track = currentSequence->GetTrackFor(bone);

 

//키사이의 값이 보간이 필요한지 판단

if(start.time = time)

{

key = start;

}

else if(start.rot == end.rot)

{

//만약 회전이 같으면 회전은 보간할 필요가 없다.

float flerpvalue = float(time-start.time)/

float(end.time-start.time);

 

key=start;

 

//위치 보간

key.pos.x = start.pos.x + flerpvalue * (end.pos.x - start.pos.x);

key.pos.y = start.pos.y + flerpvalue * (end.pos.y - start.pos.y);

key.pos.z = start.pos.z + flerpvalue * (end.pos.z - start.pos.z);

}

else

{

assert(end.time > start.time);

float flerpvalue = float(time-start.time)/

float(end.time-start.time);

 

key=start;

 

//회전 보간

SlerpQuaternions(start.rot, end.rot, flerpvalue, key.rot);

 

//위치 보간

key.pos.x = start.pos.x + flerpvalue * (end.pos.x - start.pos.x);

key.pos.y = start.pos.y + flerpvalue * (end.pos.y - start.pos.y);

key.pos.z = start.pos.z + flerpvalue * (end.pos.z - start.pos.z);

}

Matrix frame;

QuaternionToMatrix(key.rot, frame);

frame.PreTranslate(key.pos);

bone->m_offset = frame;

}

 

위의 코드를 보면 질문이 생길것이다. "애니메이션 컨트롤러는 무엇인가요?" , "애니메이션 인스턴스는 무엇인가요?". 이것들은 단지 애니메이션 데이터를 조직화하여 사용하는 함수들 이다. 그래서 같은 애니메이션(스키닝 데이터등)을 가지는 여러 캐릭터를 만들수 있다. 그것들은 본과 정점, 인덱스를 재사용이 가능하다. 그래서 최상의 성능을 내기 위해 데이터들을 각 엔티티마다의 애니메이션된 메쉬를 복제하여 사용한다. 각 애니메이션 인스턴스는 자신의 본셋, 정점, 인덱스 데이터를 가진다.

 

이제 애니메이션 컨트롤러를 자세히 설명하겠다. 애니메이션 시퀀스의 컨트롤러 컨트롤들은 재생된다. 애니메이션 인스턴스에게 어떤 시간이 사용되는지를 알려준다. 각 엔티티들은 자신만의 애니메이션 컨트롤러와 인스턴스를 가진다. 애니메이션 컨트롤러는 본이 움직일때 간단히 계산된다. 우리는 정정과 인덱스 버퍼가 매 프래임마다 갱신 되는것을 원하지 않는다. 그것은 너무 느리다. 그래서 30~60프레임마다 갱신한다. 

 

m_fCurrentAnimRate += deltatime * m_fAnimRate;

 

만약 현재 프래임율이 30보다 크다면 애니메이션 인스턴스에 스켈레톤 메쉬를 갱신하라고 요청해야한다.

 

이제 본과 본에의 영향받는 정점의 최종 변환행렬을 계산하는거에 대해 알아보자. 그것을 계산하는데에는 두가지 방법이 있다.  우리는 정점을 버텍스 쉐이더에의해서 변형되는것 처럼 각각 버텍스에 모든 본들의 변형을 알려주어서 계산할수있다. 이 방법도 갠찮지만 정점당 하나의 본만 영향을 줄수 있다. 그러나 최상의 방법은 본의 변형을 모든 정점에 영향을 주는 것이다. 다음은 최종 정점을 계산하는 코드이다.

 

void BoneMul(Bone *bone, Vector *v, float weight)

{

//본에의한 상대적인 정점의 변환을 구한다.

bone->m_init.InverseTranslateVect(&v->x);

bone->m_init.InverseRotaeVect(&v->x);

 

//최종 정점의 변환을 구한다.

Vector localPos = *v;

bone->m_final.Mul(&localPos.x);

 

//정점에 본에의한 가중치를 적용한다.

v->x = localPos.x * weight;

v->y = localPos.y * weight;

v->z = localPos.z * weight;

}

 

위의 코드는 먼저 상대적인 정점을 본의 초기 위치와 곱한다. 이유는 애니메이션된 본의 오프셋 매트릭스는 초기 위치에서 상대적으로 주어지기 때문이다. 그러면 우리는 정점을 본의 월드 매트릭스를 이용하여 최종 변환을 시킬수가 있다. 마침내 우리는 본에의해 얼마나 영향받는지를 적용할수 있게 된다. 만약 정점이 하나의 본에의해서 영향받는다면 가중치는 1이다. 그러나 만약 정점이 하나 이상에의해서 영향을 받는다면 가중치의 값은 다양해 질것이다.

이제 상대적인 월드 매트릭스(이것은 아마도 엔티티를 화면에 표시하는 매트릭스일 것이다.) 메쉬안의 모든 정점들을 변형시키는 함수를 제시 할것이다. 

BlendVertex 구조체는 여기에서 사용된다. 그것은 모든 본들이 그 정점에 얼마나 영향을 미치는가에 대한 가중치들을 가지고 있다. 

 

void Bone::Transform(float dt, AnimController *animControl)

{

animControl->Update(this, dt);

if(parentIndex > -1)

{

Bone* parent = skMesh->GetBone(parentIndex);

m_final = m_offset *parent->m_final;

}

else

{

m_final = m_init * m_offset;

}

 

//자식들을 갱신

vector<int>::iterator childIter;

for(childIter=children.begin(); childIter != children.end(); ++childIter)

{

int childIndex = (*childIter);

Bone* child = skMesh->GetBone(childIndex);

child->Transform(dt, animControl);

}

}

 

void SkelMeshInstance::ComputeSkinVerts(Matrix &worldMatrix)

{

ModelResource* resource =  GetModelResource(0);

resource->LockVertexBuffer();

 

SkeletalMesh* skMesh = SafeCase(mesh);

 

VertexDuplivation* &duplicates = skMesh->GetVertexDuplicaion();

//블렌드 정점

size_t vertices = blendVerts.size();

for(int i = 0; i < vertices; ++i)

{

BlendedVertex *bv = &blendVerts[i];

 

//모든 복제된 정점을 얻어 월드 위치를 갱신한다.

VertexDuplication *vd = duplicates[i];

//assert(vd->indices.size() > 0);

 

//정점의 월드 위치를 얻는다.

Vector pos(vd->pos.x, vd->pos.y, vd->pos.z);

 

//TODO... 3*3매트릭스로 노말을 계산

int maxInfl = (int)bv->GetNumInfluence();

if(maxInfl == 1)

{

BlendWeight *bw = &bv->GetInfluence(0);

assert(bw->weight == 1);

Bone* bone = *bones[bw->boneIndex];

BoneMul(bone, &pos, bw->weight);

}

else

{

float LastWeight = 0.0f;

//최종 결과 정점

pos.x = 0;

pos.y = 0;

pos.z = 0;

 

for(int j = 0; j < maxInfl - 1; ++j)

{

BlendWeight *bw = &bv->GetInfluence(j);

Bone* bone = &bones[bw->boneIndex];

LastWeight  = LastWeight + bw->weight;

 

Vector worldvert(vd->pos.x, vd->pos.y, vd->pos.z);

BoneMul(bone, &worldvert, bw->weight);

pos.x += worldvert.x;

pos.y += worldvert.y;

pos.z += worldvert.z;

}

 

LaseWeight = 1.0f - LastWeight;

 

Vector worldvert(vd->pos.x, vd->pos.y, vd->pos.z);

BlendWeight* lw = &bv->GetInfluence(maxInfl - 1);

BoneMul(bone, &worldvert, LastWeight);

pos.x += worldvert.x;

pos.y += worldvert.y;

pos.z += worldvert.z;

}

 

int verts = vd->indices.size();

for(int k = 0; k < verts; ++k)

{

 int index = vd->indices[k];

Vertex & vec = resource->GetVertex(index);

vec.x = pos.x;

vec.y = pos.y;

vec.z = pos.z;

 

//TODO: rotate the normals....

}

}

resource->UnlockVertexBuffer();

}

 

코드는 효율적이지 않다. 이것은 본이 하나라면 갠찮지만 실제로는 그렇지 않다. 캐시를 위한 최적화가 없기 때문이다. BlendVertex 최종 위치를 계산하기위해 각각의 매트릭스를 가진다. 빠르고 캐시 친화적인 방법은 본을 변환한 후에 최종 위치를 계산하는 것이다. 이 방법은 끊임없이 매트릭스를 push,pop을 하지 않는다.

 

void Bone::Transform(float dt, AnimController *animControl)

{

animControl->Update(this, dt);

 

if(parentIndex > -1)

{

Bone* parent = skMesh->GetBone(parentIndex);

m_final = m_offset* parent->m_final;

}

else

{

m_final = m_init * m_offset;

}

 

ModelResource* resource = skMesh->GetModelResource(0);

 

if(parentIndex == -1)

{

Vertex *v = &resource->GetVertex(0);

int numVerts = resource->GetNumVertices();

for(int i = 0; i < numVerts; ++i)

{

v[i].x = 0;

v[i].y = 0;

v[i].z = 0;

 

//TODO: 노말도 0으로

}

}

 

int numWeights = this->m_blendVerts.size();

for(int i = 0; i < numWeights; ++i)

{

BlendWeight &bw = m_blendVerts[i];

if(bw.weight < 0.000001f)

continue;

VertexDuplication* vd = mesh->GetVertexDuplicatie(bw.vertexIndex);

 

Vector pos(vd->pos.x, vd->pose.y, vd->pos.z);

 

BoneMul(this, &pos, bw.weight);

int verts = vd->indices.size();

for(int k = 0; k < verts; ++k)

{

int index = vd->indices[k];

BaseVertex *vec = resource->GetVertex(index);

vec->x += pos.x;

vec->y += pos.y;

vec->z += pos.z;

}

}

vector<int>::iterator childIter;

for(childIter = children.begin(); childIter != children.end(); ++childIter)

{

int childIndex = (*childIter);

Bone* child = skMesh->GetBone(childIndex);

child->Transform(dt, animControl);

}

}

 

 

원문 : http://www.kreationsedge.com/?page_id=29

'IT 이모저모' 카테고리의 다른 글

3D 평면  (0) 2018.03.05
스텐실 버퍼  (0) 2018.03.05
DX 11의 Stages  (0) 2018.03.05
DirectX Tools  (0) 2018.03.05
DX 11 - Direct3D 초기화  (0) 2018.03.05