본문 바로가기

DirectX11 3D/기본 문법

<DirectX11 3D> 35 - Bone, Mesh Data Read + Write File

Assimp 구조

 

필요한 개념


- Mesh와 Bone은 1대1 관계, mesh가 없는 본도 있습니다.

ex) HP바 같은 경우 가상본만 있으면 된다, 그릴 정보가 없어서

 

 

- Assimp의 aiScene에는 각종 데이터가 배열로 있다. (ex) mAnimations, mMeshs, mMaterials, mTextures 등), 그래서 인덱스를 얻어서 배열에서 참조해서 사용해야 한다.

 

 

- Assimp의 구조 사진을 보면 모든 데이터는 aiScene에서 가져온다. aiScene의 aiNode를 통해 Bone 정보와 Mesh 정보를 얻어온다. Bone은 있지만 Mesh가 없는 경우가 있다. aiNode에서 Bone 정보(Transformation)를 얻어오고, 저장해주며 재귀로 반복하고, 읽어오면서 Mesh를 읽어온다.

 

 

- aiMesh에는 mVertices(정점 데이터), mTextureCoords(Uv 데이터), mNormals(노말 데이터), mFaces(인덱스 데이터)들이 배열로 있다.

 

 

- Model을 Tower, Tank, Kachjin을 사용한다.

 

 

- Converter class는 Assimp의 Mesh, Bone 데이터를 읽어와서 우리가 정의한 데이터(*Tip : Types.h 참조)로 변환해주는 역할을 한다.

 

 

- ai라는 단어는(Assimp)의 약자이며, Assimp에서 정의한 타입의 접두어가 된다.

 

 

 

 

Converter.h


더보기
#pragma once

class Converter
{
public:
	Converter();
	~Converter();

	void ReadFile(wstring file);

public:
	//읽어 들여서 원하는 파일로 저장
	void ExportMesh(wstring savePath);

private:
	// 즉 원하는 정보를 -> 변수로 저장하고, 원하는 파일의 형태로 써준다.
	// Scene Root를 재귀를 타서 Trasformation(Bone) 정보를 가져올 것이다.
	void ReadBoneData(aiNode* node, int index, int parent);
	// Scene Root를 재귀를 타서 Mesh 정보를 가져올 것이다.]
	// bone : mesh가 참조할 자기의 Bone번호
	void ReadMeshData(aiNode* node, int bone);
	// 우리가 원하는 형태의 파일로 써준다.
	void WriteMeshData(wstring savePath);
    
private:
	// 모델 파일 주소
	wstring file;

	// Scene 로드를 위해
	Assimp::Importer* importer;
	// Model 관련된 정보가 있다.
	const aiScene* scene;

	// 우리는 접두사로 as(Assimp)를 했고
	// Assimp는 ai라는 접두사가 붙는다.
	vector<struct asBone*> bones;
	vector<struct asMesh*> meshes;
};

 

 

 

 

 

 

 

Converter.Cpp


더보기
#include "stdafx.h"
#include "Converter.h"
#include "Types.h"
#include "Utilities/BinaryFile.h"
#include "Utilities/Xml.h"

Converter::Converter()
{
	importer = new Assimp::Importer();
}

Converter::~Converter()
{
	SafeDelete(importer);
}

void Converter::ReadFile(wstring file)
{
	this->file = L"../../_Assets/" + file;

	// aiProcess_ConvertToLeftHanded : 왼손좌표계로 수정, 우리는 왼손좌표계, 행우선(가로)를 사용한다. 우리는 행우선으로
	// x,y,z하는데
	// 일반 3D 그래픽은 오른손좌표계, 열우선(세로)을 사용한다. 열우선은 세로 축 방향으로 x,y,z한다.
	
	// 우리가 아는 삼각형으로 모델을 디자인 하는 방식 외에도 스컬팅, 넙스 등 여러가지 포맷이 있다.
	// 그리는 방식이 많다.
	// 그러나 우리는 삼각형으로 그린다.
	// aiProcess_Triangulate : 삼각형으로 변환해서 데이터를 달라
	// aiProcess_GenUVCoords : 우리가 위에 삼각형으로 바꿨으니까 UV도 변환해서 달라는 얘기
	// aiProcess_GenNormals : 삼각형 단위로 바뀌었으니까, 노멀도 다시 계산해달라
	// aiProcess_CalcTangentSpace : 나중에 NormalMap할때 설명해주심

	scene = importer->ReadFile(String::ToString(this->file),
		aiProcess_ConvertToLeftHanded | aiProcess_Triangulate | aiProcess_GenUVCoords |
		aiProcess_GenNormals | aiProcess_CalcTangentSpace);

	// 부를 수 없다면 scene이 null이 나온다.
	assert(scene != nullptr);
}

void Converter::ExportMesh(wstring savePath)
{
	// 확장자 .mesh로 임의의 확장자로 저장해서 mesh 정보 저장
	savePath = L"../../_Models/" +savePath + L".mesh";

	// Root노드의 부모는 -1이다.
	ReadBoneData(scene->mRootNode, -1, -1);
	WriteMeshData(savePath);
}

void Converter::ReadBoneData(aiNode* node, int index, int parent)
{
	// 본 정보 읽기
	asBone* bone = new asBone();
	bone->Index = index;
	// C_Str() -> const char로 리턴
	bone->Name = node->mName.C_Str();
	bone->Parent = parent;

	// node->mTransformation[0] 시작주소를 넘긴다. 
	// mTransformation -> 배열이어서
	// D3DXMATRIX(CONST FLOAT*); -> 얘는 시작부터 16개 까지 끊어버림
	// 그래서 가능
	Matrix transform(node->mTransformation[0]);

	// Assimp는 열우선이어서 -> 행우선으로 바꿔준다.
	// 전치
	D3DXMatrixTranspose(&bone->Transform, &transform);

	// 부모 자식 관계를 맺어줌
	Matrix matParent;
	if (parent < 0)
		D3DXMatrixIdentity(&matParent);

	// 어떤 사람들은 부모 자식 관계를 움직일때 root를 움직이는 방법을 쓴다. -> 안좋음, 나중에 꼬임
	// 우리는 root를 Identity로 해버린다.

	else
		matParent = bones[parent]->Transform;

	
	// 항상 행렬의 곱은 왼쪽이 기준이 된다. 왼쪽을 기점으로 얼마만큼 이동할지는
	// 오른쪽 곱이 결정한다.
	// 자기를 기점으로 부모 만큼 움직일 것이다.
	bone->Transform = bone->Transform * matParent;
	bones.push_back(bone);


	// 메쉬 정보 읽기
	// 각 노드가 가지고 있는 Mesh정보를 읽어들임
	ReadMeshData(node, index);

	// 2번째 인자 : 다음 애의 인덱스가 된다.
	// 3번째 인자 : 자신의 인덱스가 자식의 부모가 된다.
	for (UINT i = 0; i < node->mNumChildren; i++)
		ReadBoneData(node->mChildren[i], bones.size(), index);
}

void Converter::ReadMeshData(aiNode* node, int bone)
{
	// Bone은 있고 Mesh는 없는 경우가 있다.
	// Mesh의 갯수가 1보다 작다면 할 필요가 없다.
	if (node->mNumMeshes < 1) return;

	asMesh* mesh = new asMesh();
	mesh->Name = node->mName.C_Str();
	// 본을 그릴때 본의 위치를 가지고 그려진다.
	// 넘어오는 인자 bone의 번호를 그대로 참조해준다.
	mesh->BoneIndex = bone;

	for (UINT i = 0; i < node->mNumMeshes; ++i)
	{
		//Mesh 정보들을 순서대로 불러다가 써줌
		// node->mMeshes에는 정보가 아닌 번호를 가지고 있다.
		// 그 배열 번호는 씬에 있는 메시 배열의 번호이다.
		// 이 번호로 씬에 있는 메쉬 정보를 가져옴
		UINT index = node->mMeshes[i];
		aiMesh* srcMesh = scene->mMeshes[index];

		// 매터리얼도 번호를 가지고 있으므로 씬의 매터리얼 배열에서 값을 가져옴
		aiMaterial* material = scene->mMaterials[srcMesh->mMaterialIndex];
		mesh->MaterialName = material->GetName().C_Str();

		// 매시마다 정점을 다가지고 있다.
		// 매시가 여러개가 되어도 정점을 계속 이어서 저장하려고
		// 이전에 쌓아놓은 크기에서 시작한다.
		UINT startVertex = mesh->Vertices.size();
		// 계속 반복면서 정점이 쌓인다.
		for (UINT v = 0; v < srcMesh->mNumVertices; v++)
		{
			Model::ModelVertex vertex;
			// 메쉬가 존재한다면 position은 항상 존재
			memcpy(&vertex.Position, &srcMesh->mVertices[v], sizeof(Vector3));


			// 텍스쳐 좌표가 없을 수도 있다.
			// 데칼으로 해서 레어어를 여러겹을 해서 텍스쳐를 여러개 쓸 수 있다.(3D 프로그래밍) 
			// -> 게임에서는 문신, 데칼을 따른 방식으로 그림(일반적으로 0번으로 고정시켜서 함)
			// 레이어 때문에 mTextureCoords는 2차원 배열이다. 그래서 0번하고 가져옴
			if (srcMesh->HasTextureCoords(0))
				memcpy(&vertex.Uv, &srcMesh->mTextureCoords[0][v], sizeof(Vector2));

			// Normal을 가지고 있다면
			if (srcMesh->HasNormals())
				memcpy(&vertex.Normal, &srcMesh->mNormals[v], sizeof(Vector3));

			mesh->Vertices.push_back(vertex);
		}

		// face라고 함, face안에는 index가 있다.
		for (UINT f = 0; f < srcMesh->mNumFaces; f++)
		{
			// 구조체 같은 것은 &로 받아야 복사가 안일어나고 빠르다.
			aiFace& face = srcMesh->mFaces[f];

			// 0, 1, 2, 2, 1, 3(정점은 4개)하면 다음 사각형 인덱스는 4번 부터 시작해야 한다.
			// 그래서 startVertex만큼 더해준다.
			for (UINT k = 0; k < face.mNumIndices; k++)
				mesh->Indices.push_back(face.mIndices[k] + startVertex);
		}

		meshes.push_back(mesh);
	}
}

void Converter::WriteMeshData(wstring savePath)
{
	//저장할때 Byte File로 저장 : txt파일보다 
	//Binary 파일이 데이터를 읽는데 훨씬 더 빠르다.
	//File* file; 을 사용해서 작성도 가능하다
	//But Window Api로 구현해 놓은 것이 있다. BinaryFile.h에 구현해 놓았다.
	
	// CreateFolder는 해당 경로의 폴더만 만든다.
	// CreateFolders는 해당 경로까지 폴더가 없으면 차례로 만들면서 내려온다.
	Path::CreateFolders(Path::GetDirectoryName(savePath));

	BinaryWriter* w = new BinaryWriter();
	w->Open(savePath);
	// 몇 바이트 읽어야 될 지 몰라서 갯수 먼저 써놓고 시작한다.
	w->UInt(bones.size());
	for (asBone* bone : bones)
	{
		// String이 없다면 구조체 단위(Byte 단위)로 쓰는 것이 빠르다.(정점 쓸때 이렇게 씀)
		w->Int(bone->Index);
		// 읽기 빠르게 할때는 문자를 256으로 고정해놓고 쓴다.(걍 한번에 구조체로 읽어들인다.)
		// 그러나 여기서 구현해 놓은 String() 함수는 그 문자 크기 만큼 저장한다.
		w->String(bone->Name);
		w->Int(bone->Parent);
		w->Matrix(bone->Transform);

		SafeDelete(bone);
	}

	w->UInt(meshes.size());
	for (asMesh* meshData : meshes)
	{
		w->String(meshData->Name);
		w->Int(meshData->BoneIndex);
		
		w->String(meshData->MaterialName);

		// vertices가 몇 바이튼지 저장해놓고 구조체로 한번에 쓴다.
		w->UInt(meshData->Vertices.size());
		// 1번째 인자 : 시작주소, 2번째 인자 : 크기
		w->Byte(&meshData->Vertices[0], sizeof(Model::ModelVertex) * meshData->Vertices.size());


		w->UInt(meshData->Indices.size()); 
		w->Byte(&meshData->Indices[0], sizeof(UINT) * meshData->Indices.size());

		SafeDelete(meshData);
	}


	// Close() 안하면 파일 접근 불가
	w->Close();
	SafeDelete(w);
}

 

 

 

 

Types.h

 


더보기
//Mesh와 Bone은 1대1 관계, mesh가 없는 본도 있습니다.(ex)HP바 같은 경우 가상본만 있으면 된다., 그릴 정보가 없어서)


// Bone 정보를 저장
// 부모 자식 관계르 저장
struct asBone
{
	int Index;
	string Name;
	
	// Parent(Index)가 -1이라면 루트
	int Parent; // Bone의 부모(포탑의 부모 등...)
	Matrix Transform; // Bone의 행렬
};

// Mesh 정보를 저장
struct asMesh
{
	// Bone의 Name과 동일
	string Name;
    
	// 자신이 그려질 bone의 index
	int BoneIndex;

	aiMesh* Mesh;

	// 참조할 재질의 이름
	string MaterialName;

	// 정점형태
	vector<Model::ModelVertex> Vertices;
	// 인덱스
	vector<UINT> Indices;
};

 

 

ModelMesh.h


더보기
#pragma once

// 이 안에다 Bone과 Mesh를 같이 넣어놓는다.

class ModelBone
{
public:
	friend class Model;

private:
	// Model에서만 생성할 수 있도록
	ModelBone();
	~ModelBone();

public:
	int Index() { return index; }

	int ParentIndex() { return parentIndex; }
	ModelBone* Parent() { return parent; }

	wstring Name() { return name; }

	Matrix& Transform() { return transform; }
	void Transform(Matrix& matrix) { transform = matrix; }

	vector<ModelBone*>& Childs() { return childs; }

private:
	int index;
	wstring name;

	int parentIndex;
	ModelBone* parent;

	Matrix transform;
	// Model에서 Binding을 통해 세팅해줄것임
	vector<ModelBone*> childs;
};


///////////////////////////////////////////////////////////////////////

// Renderer로 부터 상속받아서 해도 되지만, Transform 콜 소지가 있어서
// 그냥 작성한다.
class ModelMesh
{
public:
	friend class Model; 

private:
	// Model에서만 생성할 수 있도록
	ModelMesh();
	~ModelMesh();

	// Material 때문에 받는다.
	void Binding(Model* model);

public:
	void Pass(UINT val) { pass = val; }
	void SetShader(Shader* shader);

	void Update();
	void Render();

	wstring Name() { return name; }

	int BoneIndex() { return boneIndex; }
	class ModelBone* Bone() { return bone; }

	// Matrix Transforms[MAX_MODEL_TRANSFORMS]를 복사해 주기 위한 함수
	void Transforms(Matrix* transforms);

	// Transform(Model의 위치를 움직일), world를 넣어줌
	void SetTransform(Transform* transform);

private:
	// Bone 정보를 넘길 것이다.
	struct BoneDesc
	{
		// why 배열로 넘기나? 하나만 넘겨도 된다. index도 필요없다.
		// 그러나 이후에 애니메이션 할때 필요하기 때문이다.

		// 전체 본을 배열로 만든다음에 배열을 넘긴다는 개념
		// 본의 전체개수는 250개로 설정한다.
		// 사람의 관절이 250개가 넘지 않기 때문
		// 본 전체의 행렬 배열과
		Matrix Transforms[MAX_MODEL_TRANSFORMS];

		// 그 배열에서 현재 그려질 매쉬가 참조할 본의 번호가 같이
		// 넘어간다.
		// Bone배열에서 Index값으로 Bone을 선택한다.
		UINT Index;
		float Padding[3];
	} boneDesc;

private:
	wstring name;

	Shader* shader;
	UINT pass = 0;

	Transform* transform = nullptr;
	PerFrame* perFrame = nullptr;

	wstring materialName = L"";
	// Material* material;

	int boneIndex;
	class ModelBone* bone;

	VertexBuffer* vertexBuffer;
	UINT vertexCount;
	Model::ModelVertex* vertices;

	IndexBuffer* indexBuffer;
	UINT indexCount;
	UINT* indices;

	ConstantBuffer* boneBuffer;
	ID3DX11EffectConstantBuffer* sBoneBuffer;
};

 

ModelMesh.Cpp


더보기
#include "Framework.h"
#include "ModelMesh.h"

ModelBone::ModelBone()
{
}

ModelBone::~ModelBone()
{
}

///////////////////////////////////////////////////////////////////////

ModelMesh::ModelMesh()
{
	boneBuffer = new ConstantBuffer(&boneDesc, sizeof(BoneDesc));
}

ModelMesh::~ModelMesh()
{
	SafeDelete(transform);
	SafeDelete(perFrame);

	SafeDelete(material);

	SafeDeleteArray(vertices);
	SafeDeleteArray(indices);

	SafeDelete(vertexBuffer);
	SafeDelete(indexBuffer);

	SafeDelete(boneBuffer);
}

void ModelMesh::Binding(Model* model)
{
	// VertexBuffer, IndexBuffer 만드는 것 세팅
	vertexBuffer = new VertexBuffer(vertices, vertexCount, sizeof(Model::ModelVertex));
	indexBuffer = new IndexBuffer(indices, indexCount);

	// Material* srcMaterial = model->MaterialByName(materialName);

	// material = new Material();
	// material->CopyFrom(srcMaterial);
}

void ModelMesh::SetShader(Shader* shader)
{
	// 모델을 렌더링하는 Shader가 바뀌면 그거에 따른거 처리
	this->shader = shader;

	// Perframe, Transform은 Shader를 세팅해서 해당 Shader에 값을 미뤄주도록 처리되어있다.
	// 그래서 새로 세팅
	SafeDelete(transform);
	transform = new Transform(shader);

	SafeDelete(perFrame);
	perFrame = new PerFrame(shader);

	sBoneBuffer = shader->AsConstantBuffer("CB_Bone");

	// material->SetShader(shader);
}

void ModelMesh::Update()
{
	boneDesc.Index = boneIndex;

	// 본 배열에서 하나 선택한 것이랑 월드랑 결합해서 최종위치를 구해낼것이다.

	perFrame->Update();
	transform->Update();
}


void ModelMesh::Render()
{
	boneBuffer->Render();
	sBoneBuffer->SetConstantBuffer(boneBuffer->Buffer());

	perFrame->Render();
	transform->Render();
	material->Render();

	vertexBuffer->Render();
	indexBuffer->Render();

	// 우리가 aiProcess로 삼각형 단위로 해달라고 해서, 이렇게 하면된다.
	D3D::GetDC()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	shader->DrawIndexed(0, pass, indexCount);
}

void ModelMesh::Transforms(Matrix* transforms)
{
	memcpy(boneDesc.Transforms, transforms, sizeof(Matrix) * MAX_MODEL_TRANSFORMS);
}

void ModelMesh::SetTransform(Transform* transform)
{
	// world를 넣어줌
	this->transform->Set(transform);
}

 

ModelDemo.h


더보기
#pragma once
#include "Systems/IExecute.h"

class ModelDemo : public IExecute
{
public:
	virtual void Initialize() override;
	virtual void Ready() override {};
	virtual void Destroy() override {};
	virtual void Update() override;
	virtual void PreRender() override {};
	virtual void Render() override;
	virtual void PostRender() override {};
	virtual void ResizeScreen() override {};

private:
	void AirPlane();
	void Tower();
	// 탱크 모델을 불러들임
	void Tank();
	void Kachujin();

private:
	Shader* shader;

	ModelRender* airPlane = nullptr;
	ModelRender* tower = nullptr;
	ModelRender* tank = nullptr;
	ModelRender* kachujin = nullptr;

	CubeSky* sky;
	
	Vector3 direction = Vector3(-1, -1, +1);
	
	Shader* gridShader;
	MeshGrid* grid;
};

 

 

 

 

 

ModelDemo.cpp

 


더보기
#include "stdafx.h"
#include "ModelDemo.h"
#include "Converter.h"

void ModelDemo::Initialize()
{
	Context::Get()->GetCamera()->RotationDegree(20, 0, 0);
	Context::Get()->GetCamera()->Position(1, 36, -85);

	shader = new Shader(L"38_Model.fx");

	AirPlane();
	Tower();
	Tank();
	Kachujin();

	sky = new CubeSky(L"Environment/GrassCube1024.dds");

	gridShader = new Shader(L"25_Mesh.fx");
	grid = new MeshGrid(gridShader, 6, 6);
	grid->GetTransform()->Scale(12, 1, 12);
	grid->DiffuseMap(L"Floor.png");
}

void ModelDemo::Update()
{
	sky->Update();
	grid->Update();

	if (airPlane != nullptr) airPlane->Update();
	if (tower != nullptr) tower->Update();
	if (tank != nullptr)
	{
		// 디버깅 모드로 보면 10번 본은 포탑이다.
		ModelBone* bone = tank->GetModel()->BoneByIndex(10);

		// 회전 시키기 위해
		Transform transform;

		// 180 만큼 돌기 위해 PI 곱해줌
		float rotation = sinf(Time::Get()->Running() + 100) * Math::PI * Time::Delta();
		transform.Rotation(0, rotation, 0);

		tank->UpdateTransform(bone, transform.World());
		tank->Update();
	}
	if (kachujin != nullptr) kachujin->Update();
}


void ModelDemo::Render()
{
	ImGui::SliderFloat3("Direction", direction, -1, +1);
	shader->AsVector("Direction")->SetFloatVector(direction);
	gridShader->AsVector("Direction")->SetFloatVector(direction);

	static int pass = 0;
	ImGui::InputInt("Pass", &pass);
	pass %= 2;

	sky->Render();
	grid->Render();

	if (airPlane != nullptr)
	{
		airPlane->Pass(pass);
		airPlane->Render();
	}

	if (tower != nullptr)
	{
		tower->Pass(pass);
		tower->Render();
	}

	if (tank != nullptr)
	{
		tank->Pass(pass);
		tank->Render();
	}

	if (kachujin != nullptr)
	{
		kachujin->Pass(pass);
		kachujin->Render();
	}
}

void ModelDemo::AirPlane()
{
	airPlane = new ModelRender(shader);
	airPlane->ReadMesh(L"B787/Airplane");
	airPlane->GetTransform()->Scale(0.005f, 0.005f, 0.005f);
}

void ModelDemo::Tower()
{
	tower = new ModelRender(shader);
	tower->ReadMesh(L"Tower/Tower");
	tower->GetTransform()->Position(-20, 0, 0);
	tower->GetTransform()->Scale(0.01f, 0.01f, 0.01f);
}

void ModelDemo::Tank()
{
	tank = new ModelRender(shader);
	tank->ReadMesh(L"Tank/Tank");
	tank->GetTransform()->Position(20, 0, 0);
}

void ModelDemo::Kachujin()
{
	kachujin = new ModelRender(shader);
	kachujin->ReadMesh(L"Kachujin/Mesh
	kachujin->GetTransform()->Position(0, 0, -30);
	kachujin->GetTransform()->Scale(0.025f, 0.025f, 0.025f);
}

 

 

 

 

 

결과


문제1)

바퀴나, 기타 다른부분이 렌더링이 안되었다.

이유) 부모 자식 관계가 제대로 맺어지지 않아서