Programming/컴퓨터그래픽스 (DX 11)

[DirectX 11] 3D 모델 로드하기 - Rigid Skeletal Mesh

양양줘 2025. 10. 29. 17:03

지난 글에서는 Static Mesh를 로드하고

DirectX 11로 화면에 띄워보는 과정을 정리하였다.

 

이번은 3D 모델 로드하기 2단계

Rigid Skeletal Mesh 띄워서 애니메이션 재생하기이다!!

 

 

좌표계 (Space)


 Static Mesh와 Skeleta Mesh 처리의 차이점을 이해하려면 vertex들의 좌표계를 확실히 이해하고 있어야 한다. 우리는 3D 모델을 Import하여 vertex 데이터들을 저장한다. 이때 3D 모델이 Static Mesh이냐, Skeletal Mesh이냐에 따라서 저장 옵션이 다르고, 이후 처리 과정이 달라진다. 아래 정리는 DirectX11에서 assimp 라이브러리로 모델을 임포트하는 환경을 기준으로 작성한다.

// 어느 Space 기준의 좌표들일까?
struct Vertex
{
    Vector3 position;   
    Vector3 normal;     
    Vector3 tangent;    
    Vector3 bitangent;  
    Vector2 texcoord;   
};

 

vertex 데이터들은 'Mesh Space -> Model Space -> World Space'의 변환 과정을 거친다. 조금 더 자세히 말하면 world space로 변환된 좌표에 view행렬을 적용하고 투영하여 최종적으로 clip space로 보내지만, 그 부분은 static mesh나 skeletal mesh나 공통 과정이기 때문에 생략한다. 3D 모델 리소스에 들어있는 vertex 데이터들은 Mesh Space 즉, 자신이 속한 노드의 원점을 기준으로 한 데이터들로 저장되어있다. 때문에 world space 좌표를 얻기 위해서는 model space 변환 과정을 한번 거쳐야한다. Model Space란 모델 전체 기준의 좌표계로, 노드 트리 구조에서 부모-자식 관계에 따라 부모 transform을 누적 적용한 값이 바로 model space기준의 값이 된다.

 

좌표계  설명 기준
Mesh  Space (Local Space) 노드(메시) 자신을 기준으로 한 좌표 Node 기준
Model Space 모델 루트(0,0,0)를 기준으로 한 좌표 모델 전체 기준
World Space 게임 월드의 절대 좌표 씬 전체 기준

 

전 글에서 StaticMesh를 로드할 때 assimp 라이브러리의 aiProcess_PreTransformVertices옵션을 사용했었다. 이 옵션은 각 노드에 저장되어있는 local space 기준 좌표를 Model Space기준의 좌표로 변환하여 저장해주는 옵션이었다. 하지만 값이 그렇게 저장되면 계층 구조를 잃기 때문에 static mesh에만 사용하며, 계층구조 기반의 애니메이션을 사용하는 skeletal mesh에는 사용할 수 없다. 때문에 RigidSkeletalMesh와 SkinnedSkeletalMesh는 mesh를 Local Space기준으로 저장하고, 매 연산시마다 부모 행렬을 누적시켜 Model Space를 계산해주어야 한다.

 

⭐ 요약

  • Static Mesh -> aiProcess_PreTransformVertices 플래그를 사용해 Model Space 기준 데이터 저장, 부모-자식 관계 필요 x
  • Skeletal Mesh -> aiProcess_PreTransformVertices 사용 x, Local Space 기준 데이터로 저장하고 매 연산시마다 부모 누적

 

 

 


바인드 포즈 (Bind Pose)


 바인드 포즈(BInd Pose)애니메이션이 재생되기 전 모델의 기준 자세를 말한다. 즉, 메시와 뼈대가 처음 연결 될 때(리깅) 기준 자세와 기준 좌표 상태를 의미한다. 이 상태에서 모든 Bone의 Transform은 초기값(Identity 또는 기본 위치)이며 메시의 vertex들도 해당 Bone에 정확히 붙어 있는 위치에 배치된다. StaticMesh의 경우 뼈대가 없기 때문에 해당되지 않으며, SkeletalMesh의 경우 각 노드에 저장되어있는 transformaltion이 이 바인드 포즈에 대한 변환 행렬 값이다.

 

 

 

 


Skeletal Mesh


 Skeletal Mesh뼈대(Skeletal)와 스킨(Skin)으로 구성되어 애니메이션이 가능한 3D 메시로, 하나 이상의 Bone에 의해 변형되는 메시를 말한다. 각 Bone은 위치, 회전, 스케일 정보를 가지며, 애니메이션이 실행되면 각 Bone의 변환이 시간에 따라 변화하고 그에 맞게 Mesh가 변형되며 애니메이션이 보여진다. (assimp 관점에서는 node별 transfrom 데이터가 저장되어있으며, Bone은 offset과 가중치 데이터를 저장하여 skinning을 담당한다)

  • Skeleta Mesh의 구성 요소
    • Skeleton (Bone, 골격) : 뼈(Bone)들의 계층 구조 (트리 구조)
    • Mesh (스킨) : 실제 표면(geometry) — 버텍스(vertex)들로 구성
    • Skinning 정보 : 각 버텍스가 어떤 뼈(bone)에 얼마나 영향을 받는지 (weight)

 

Rigid Skeletal Mesh

 

 Rigid Skeletal Mesh리깅을 하지 않은, 각 vertex가 하나의 Bone(=Node)에만 100% 영향을 받는 스켈레탈 메시를 의미한다. 리깅을 하지 않았기 때문에 씬그래프 관점에서 Bone은 Node와 동일시 할 수 있다. Rigid Skeletal Mesh의 vertex는 Local Space의 데이터가 저장되어있으며, animation data 또한 Local Space의 키프레임 값이 저장되어있다. 

  • 1 Node = 1 Bone
  • 각 버텍스는 1개의 Bone( = Node)에만 영향을 받음
  • 저장된 Vertex 데이터는 Local Space 기준 , 매 연산시 Model Space 변환 필요

 

 

Skinned Skeletal Mesh

 

 Skinned Skeletal Mesh 각 vertex가 여러 Bone에 가중치(Weight) 로 영향을 받는 스켈레탈 메시를 의미하며, 일반적으로 “Skeletal Mesh”라 하면 이 Skinned Skeletal Mesh를 의미한다. Skinned Mesh의 vertex는 여러 Bone의 변환 행렬을 가중합(weighted sum) 하여 최종 위치를 계산한다. Skinned Skeletal Mesh의 vertex는 Rigid Skeletal Mesh와 마찬가지로 Local Space의 데이터가 저장되어있으며, animation data 또한 Local Space의 키프레임 값이 저장되어있다.

  • 1 Node : 多 Bone
  • 각 버텍스는 여러 Bone에 가중치 영향을 받음
  • Vertex 데이터는 Local Space 기준으로 저장, 매 연산시 Model Space 변환 필요

 


assimp의 Animation Data


Assimp로 애니메이션이 포함된 3D 모델 파일을 로드하면, aiScene 객체 내에 aiAnimation 배열이 생성된다. 각 aiAnimation은 하나의 애니메이션 클립(Animation Clip) 을 의미하며, 예를 들어 걷기(Walk), 달리기(Run) 등의 애니메이션이 각각 aiAnimation 객체로 저장된다. aiAnimation 내부에는 여러 개의 aiNodeAnim 데이터가 포함되어 있는데, 각각의 aiNodeAnim은 모델 계층 구조 상의 특정 노드(aiNode)에 대한 키프레임(Position, Rotation, Scale) 데이터를 담고 있다. 요약하자면, aiAnimation이 전체 애니메이션 클립을 표현한다면, aiNodeAnim은 그 클립 안에서 하나의 노드가 시간에 따라 어떻게 변하는지를 정의한 데이터이다. 따라서, 모델을 로드할 때 aiScene의 mAnimations 배열을 저장해두고 Mesh의 Local Matrix에 적용함으로써 애니메이션을 실행할 수 있다.

 

aiAnimation

 

 aiAnimation은 하나의 애니메이션에 대한 정보를 담고 있는 구조체로, 애니메이션 클립(Animation Clip) 을 의미한다. 애니메이션의 실제 변환(위치, 회전, 스케일) 정보는 aiNodeAnim 포인터 배열로 저장되어 있으며, 각 aiNodeAnim은 aiNode 단위로 aiNode의 원점을 기준으로 한 변화값인 키프레임 데이터를 가지고 있다.

struct aiAnimation {
    string name;                // 애니메이션 이름 ("Walk")
    double duration;            // 전체 프레임 수
    double ticksPerSecond;      // 초당 프레임 수
    unsigned int numChannels;   // aiNodeAnim 개수
    aiNodeAnim** channels;      // Node별 애니메이션
};

 

aiNodeAnim

 

 aiNodeAnim 하나의 노드(aiNode)에 대한 키프레임 배열을 저장한다. 즉, 특정 노드가 시간에 따라 어떻게 이동(Position), 회전(Rotation), 크기(Scaling)이 변하는지를 저장하며, 이 값은 노드를 원점으로 한 변화값이다. aiNodeAnim은 자신이 어떤 노드에 대한 변화값을 가지고있는지 nodeName으로 구분하고 있기 때문에, 각 aiMesh(서브메시)를 저장할 때 nodeName을 저장해놔야 키프레임 값 조회가 가능하다.

struct aiNodeAnim {
    string nodeName;                  // 애니메이션 대상 Node 이름
    vector<aiVectorKey> positionKeys;
    vector<aiQuatKey> rotationKeys;
    vector<aiVectorKey> scalingKeys;
};

 

 


Rigid Skeletal Mesh 로드하고 애니메이션 재생하기


 Rigid Skeletal Mesh의 애니메이션을 실행하기 위한 과정은 다음과 같다. import단계에서 StaticMesh의 처리방식과 구조적으로 동일하며, Mesh의 추가 데이터와 aiScene->aiAnimation의 애니메이션 데이터를 더 저장해주면 된다.

 

1. Local Space로 데이터 저장  (Import 단계)

 StaticMesh와 SkeletalMesh의 import시에 가장 큰 차이점은 vertex 정보들을 Local Space로 저장하느냐, Model Space로 저장하느냐이다. Static Mesh의 경우 부모-자식 계층구조가 필요 없어 aiProcess_PreTransformVertices플래그를 사용해 Model Space값으로 저장하였지만, SkeletalMesh의 경우 애니메이션 변화값에 따라 매 프레임마다 부모의 변화까지 누적시켜주어야 하기 때문에 부모-자식 계층의 유지가 필요하다. 즉, Skeletal Mesh는 aiProcess_PreTransformVertices 플래그를 절대 사용하면 안되고, vertex 데이터들은 Local Space로 저장한다.

// Import Flag (aiProcess_PreTransformVertices x)
unsigned int skeletalImportFlags =
aiProcess_Triangulate |                             // vertex 삼각형 으로 출력
aiProcess_GenNormals |                              // normal 
aiProcess_GenUVCoords |                             // uv
aiProcess_CalcTangentSpace |                        // tangent vector
aiProcess_ConvertToLeftHanded;                      // DX용 왼손좌표계 변환

// Rigid Mesh Load
const aiScene* scene = importer.ReadFile(modelPath, skeletalImportFlags);

 

 

2. 부모-자식 정보 유지를 위해 parent index 저장, 애니메이션 키프레임 조회를 위해 node name 저장  (노드 순회 단계)

 assimp로 모델을 로드하면 하나의 Node가 여러개의 SubMesh로 나뉠 수 있다. 하지만 assimp에서 부모-자식 관계는 Node 단위로 이루어져 있지만 우리는 aiMesh(서브메시) 단위로 메시를 저장하기 때문에, aiNode를 순회하며 aiMesh를 처리할 때 parent index를 계산해서 따로 저장해주어야한다. 이렇게 저장한 parent index를 통해 update()시에 부모의 model 변환 행렬을 자신의 local에 적용하여 model space 변환 행렬을 만들 수 있다.

 

마찬가지로 우리는 서브메시 단위로 메시를 저장하고 그리지만, 애니메이션 키프레임 값이 들어있는 구조체는 NodeName으로 자신이 어떤 노드에 대한 변환 값을 가지고있는지 구분한다. 때문에 aiNode를 순회하며 aiMesh를 처리할 때 자신이 속한 aiNode의 이름을 서브메시에도 저장해주어야한다.  이렇게 저장한 nodeName을 통해 Udpate()시마다 자신의 키프레임값(position, rotation, scale)을 조회하여 변환행렬을 만들고, 자신(서브메시)의 Local로 적용하면 된다.

// 이 코드는 개선이 필요하다. 의미적으로만 참고하기!
// Node 순회 (초기 parent index = -1)
void ModelLoder::ProcessRigidNode(aiNode* node, const aiScene* scene, RigidMesh* rigidMesh, int parentIndex)
{
    // SubMesh 처리
    for (unsigned int i = 0; i < node->mNumMeshes; i++)
    {
        unsigned int meshIndex = node->mMeshes[i];
        aiMesh* mesh = scene->mMeshes[meshIndex];
        ProcessRigidMesh(mesh, scene, rigidMesh, node, parentIndex);
    }

    // 자식 노드 재귀 탐색
    int myIndex = rigidMesh->subMeshes.size();
    for (unsigned int i = 0; i < node->mNumChildren; i++)
    {
        ProcessRigidNode(node->mChildren[i], scene, rigidMesh, myIndex);
    }
}

// aiMesh(서브메시) 저장
void ModelLoder::ProcessRigidMesh(aiMesh* mesh, const aiScene* scene, RigidMesh* rigidMesh, aiNode* node, int parentIndex)
{
    RigidSubMesh submesh;
    submesh.nodeName = node->mName.C_Str();    // node name
    submesh.parentIndex = parentIndex -1;      // parent index
  ...   
 }

 

 

 

3. 애니메이션 실행 (Update, Render 단계)

 우리는 Node를 순회하며 RigidMesh라는 객체에 SubMesh배열, Material배열, Animation배열을 모두 저장했을 것이다. 이제는 update, render 단계에서 vertex의 local, model transform 변환 행렬을 구해 상수버퍼로 넘겨주기만 하면 애니메이션이 잘 실행 된다.

 

 1) subMesh를 순회하며 Local Space변환 행렬(Animation)을 계산한다. (애니메이션 키프레임은 보간하여 추출한다)

  우리는 각 서브메시에 자신이 속한 nodeName을 저장해두었었다. 이 nodeName을 활용하여 Animation Key Frame을 조회하고 position, rotation, scale 값으로 변환 행렬을 만들어 Local 변환 행렬로 저장한다. 여기까지 하면 애니메이션이 돌아가긴 하지만 Local Space 즉, 각 노드의 원점을 기준으로 한 변환이라 모델 전체적으로 연결이 되지 않을 것이다.

// local matrix update
// animaton key frame값을 보간해서 local matrix 업데이트
// TODO :: 해시테이블로 바꾸기
for (auto& sub : subMeshes)
{
	AnimationClip& clip = animationClips[0];
	for (auto& nodeAnim : clip.nodeAnimations){
        // nodeName으로 키프레임을 조회하여 local 변환 행렬 지정
		if (nodeAnim.nodeName == sub.nodeName) {
			Vector3 pos;  Quaternion rot;	Vector3 scl;
			nodeAnim.Interpolate(currentAnimTime, pos, rot, scl);

			sub.localMatrix = Matrix::CreateScale(scl) *
								Matrix::CreateFromQuaternion(rot) *
								Matrix::CreateTranslation(pos);
			break;
		}
		else {
            // 애니메이션이 없는 메시라면 bind 변환 행렬 지정
			sub.localMatrix = sub.bindMatrix;
		}
	}
}

 

 2) subMesh를 순회하며 Model Space 변환 행렬을 계산한다.

  submesh를 순회하며 저장해두었던 parent index로 부모 서브메시를 찾아 부모의 model 변환행렬을 자신에게 적용한다. 이렇게 부모-자식 계층이 다시 연결되어 Model Space로 변환을 할 수 있게 되는 것이다.

// model matrix update
for (auto& sub : subMeshes)
{
	if (sub.parentIndex != -1)
		sub.modelMatrix = sub.localMatrix * subMeshes[sub.parentIndex].modelMatrix;
	else
		sub.modelMatrix = sub.localMatrix;
}

 

 

 3) 상수 버퍼에 model 변환행렬과 world 변환행렬을 전달한다.

  Static Mesh Render시에 우리는 원래 모델 전체의 world 변환 행렬을 넘겼었다. 그 이유는 이미 vertex 데이터들이 model space로 저장되어있었기 때문이다. 하지만 skeletal mesh의 경우는 local space를 기준으로 vertex 데이터들이 저장되어있기 때문에 위에서 따로 model space 변환 행렬을 구해 저장해두었다. 이 model 변환행렬을 따로 보내건, world와 행렬곱을 통해 한번에 보내건 상관 없다. 결국 vertex shader에서 local space에 있는 position, normal, tangent 등 좌표값들이 local -> model -> world로 변환이 되게 하면 된다. 아래 코드는 각 sub mesh의 model 변환 행렬과 모델의 world 변환 행렬을 곱해 한번에 보낸 예시이다.

// model Matirx : 각 서브메시의 애니메이션 local 변환 행렬 * 부모 누적 model 변환 행렬
// world : 모델 전체의 world space 변환 행렬
cb.world = (sub.modelMatrix * world).Transpose();

 

 

 

 

 

 

자세한 코드는 하단 링크의  8_FBX 프로젝트를 확인하면 된다.

ModelImpoter, RigidMesh, RigidSubMesh, AnimationClip 파일을 확인하면 된다!

다음은.. skinned skeletal mesh animation이다.... Bone의 처리가 필요하다!!!

 

 

 


 

 

GitHub - wooj22/DirectX3D11_Study: DirectX3D11 공부용 레포지토리입니다.

DirectX3D11 공부용 레포지토리입니다. Contribute to wooj22/DirectX3D11_Study development by creating an account on GitHub.

github.com