프로그래밍 공부
작성일
2023. 11. 3. 17:35
작성자
WDmil
728x90

FBX파일에서 Png파일을 모두 가져왔었으니 이번에는 Mesh데이터를 가져오고 저장, 불러오기를 해야한다.

 

전에 만들어두었던 ModelExporter에 값을 저장하는 함수를 제작한다.

void ModelExporter::ReadMesh(aiNode* node)
{
	FOR(node->mNumMeshes)
	{
		// 새로운 MeshData 객체를 생성
		MeshData* mesh = new MeshData();

		// Mesh의 이름을 설정, node->mName.C_Str()는 C 문자열로 Mesh의 이름을 제공합니다.
		mesh->name = node->mName.C_Str();

		// 현재 처리 중인 Mesh에 대한 포인터를 가져옵니다.
		aiMesh* srcMesh = scene->mMeshes[i];

		// Mesh의 재질(Material) 인덱스를 MeshData 객체에 할당
		mesh->materialIndex = srcMesh->mMaterialIndex;

		// Mesh의 정점(Vertex) 데이터를 복사
		mesh->vertices.resize(srcMesh->mNumVertices);

		FOR(srcMesh->mNumVertices)
		{
			ModelVertex vertex;

			// 정점 위치 정보를 복사
			memcpy(&vertex.pos, &srcMesh->mVertices[i], sizeof(Float3));

			// 텍스처 좌표 정보가 있는 경우, 복사
			if (srcMesh->HasTextureCoords(0))
				memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][i], sizeof(Float2));

			// 법선 벡터 정보가 있는 경우, 복사
			if (srcMesh->HasNormals())
				memcpy(&vertex.normal, &srcMesh->mNormals[i], sizeof(Float3));

			// 접선 벡터 및 이접선 벡터 정보가 있는 경우, 복사
			if (srcMesh->HasTangentsAndBitangents())
				memcpy(&vertex.tangent, &srcMesh->mTangents[i], sizeof(Float3));

			// MeshData의 정점 목록에 현재 정점 정보를 추가
			mesh->vertices[i] = vertex;
		}

		// Mesh의 인덱스 데이터를 복사 복사하기위해 배열을 resize로 늘려줌.
		mesh->indices.resize(srcMesh->mNumFaces * 3);
		FOR(srcMesh->mNumFaces)
		{
			// face = 그리기순서 얼굴, 면 이라고 생각하면됨.
			aiFace& face = srcMesh->mFaces[i];

			// 각 면(Face)의 인덱스를 MeshData 객체의 인덱스 목록에 추가
			for (UINT j = 0; j < face.mNumIndices; j++)
			{
				mesh->indices[i * 3 + j] = face.mIndices[j];
			}
		}

		// MeshData 객체를 meshes 벡터에 추가
		meshes.push_back(mesh);
	}

	FOR(node->mNumChildren)
	{
		// 현재 노드의 자식 노드에 대해 재귀적으로 Mesh를 읽고 처리 메쉬는 트리형태로 되어있기에 재귀로 읽어야함.
		ReadMesh(node->mChildren[i]);
	}
}

void ModelExporter::WriteMesh()
{
	// 저장할 파일 경로를 구성 (예: "Models/Meshes/모델이름.mesh")
	string path = "Models/Meshes/" + name + ".mesh";

	// 필요한 폴더 구조를 생성하는 함수 호출
	CreateFolders(path);

	// BinaryWriter를 사용하여 파일을 생성하고 열기
	BinaryWriter* writer = new BinaryWriter(path);

	// Mesh 데이터의 개수를 파일에 쓰기 (meshes 벡터의 크기)
	writer->UInt(meshes.size());

	/*
		미리 데이터 개수를 쓰는 이유,
		구조화된 데이터를 관리하는데 더 효율적이기 때문,
		파일의 구조상 헤더에 데이터 범위를 미리 적는것처럼
	*/

	// meshes 벡터에 있는 각 MeshData 객체를 처리
	for (MeshData* mesh : meshes)
	{
		// Mesh의 이름을 파일에 문자열 형태로 쓰기
		writer->String(mesh->name);

		// Mesh의 재질(Material) 인덱스를 파일에 쓰기
		writer->UInt(mesh->materialIndex);

		// Mesh의 정점 데이터 개수를 파일에 쓰기
		writer->UInt(mesh->vertices.size());

		// Mesh의 정점 데이터를 파일에 이진 형태로 쓰기 (정점 구조체의 배열)
		writer->Byte(mesh->vertices.data(), sizeof(ModelVertex) * mesh->vertices.size());

		// Mesh의 인덱스 데이터 개수를 파일에 쓰기
		writer->UInt(mesh->indices.size());

		// Mesh의 인덱스 데이터를 파일에 이진 형태로 쓰기 (정수 배열)
		writer->Byte(mesh->indices.data(), sizeof(UINT) * mesh->indices.size());

		// 현재 처리한 MeshData 객체를 삭제 (메모리 해제)
		delete mesh;
	}
	// 어차피 정점데이터는 읽어도 파악이 힘들기 때문에, 이진데이터 형태로 압축률이 뛰어나게 저장한다.
	// meshes 벡터를 비우고, BinaryWriter를 닫고 삭제
	meshes.clear();
	delete writer;
}

 

메쉬데이터를 FBX에서 가져온다음에 저장하는 함수이다.


 

Mesh데이터는 일반적인 Vertex와 Indices를 사용하지만, FBX파일에서 가져오는 데이터는 더 많은 값을 필요로 하기에 전용 객체를 만들어준다.


Model용 객체 제작

#pragma once
typedef VertexUVNormalTangent ModelVertex;

// 일반적인 데이터와 같으나, Model전용 meshData를 저장하는 구조체가 따로 필요하다.
// 모델에는 materialIndex데이터가 들어가야하고, 일반적인 Mesh구조체와 다르게 작성되기 때문,
class MeshData
{
public:
	string name;
	UINT materialIndex;

	vector<ModelVertex> vertices;
	vector<UINT> indices;
};

각 항목당, MeshData를 저장하는 구조체를 만들어서 관리한다.

 

이름과, 현재 객체의 materialIndex, 나머지는 같은 vertices와 indices이다.

 

class Model : public Transform
{
public:
	Model(string name);
	~Model();

	void Render();
	void GUIRender();

	void SetShader(wstring file);

private:
	void ReadMaterial();
	void ReadMesh();

protected:
	string name;
	vector<Material*> materials;
	vector<ModelMesh*> meshes;

	// 모델과 메시의 정보가 여러개임으로 한번에 worldBuffer로 관리한다.
	MatrixBuffer* worldBuffer;
};

 

Model데이터이다. 전에 만들었던 Sphere이나, Cube와 같은 방식으로 제작한다.

 

전용 구조체와 Materials와 meshs로 구분하는 차이가 있다.

 

모든 vertices가 여러개 로 구성되어있음으로, 전부 관리하기 위해 전용 worldBuffer를 통해 pos, ro, scail을 관리한다.


void Model::ReadMaterial()
{
    string file = "Models/Materials/" + name + "/" + name + ".mats";
    // Material 데이터 파일의 경로를 생성합니다. 이 경로는 Material 파일이 저장된 디렉터리와 파일 이름으로 구성됩니다.

    BinaryReader* reader = new BinaryReader(file);
    // Material 데이터 파일을 읽기 위한 BinaryReader를 생성하고 파일을 엽니다.

    if (reader->IsFailed())
        assert(false);
    // 파일 열기에 실패한 경우, assert 함수를 사용하여 프로그램 실행을 중단합니다.

    UINT size = reader->UInt();
    // Material 데이터의 개수를 파일에서 읽어옵니다.

    materials.reserve(size);
    // Material 데이터를 저장할 materials 벡터를 사전 예약합니다.

    FOR(size)
    {
        Material* material = new Material();
        // Material 클래스의 인스턴스를 생성합니다.

        material->Load(reader->String());
        // Material 데이터 파일에서 Material 정보를 읽어와서 Material 객체에 로드합니다.

        materials.push_back(material);
        // Material 객체를 materials 벡터에 추가합니다.
    }

    delete reader;
    // 사용이 끝난 BinaryReader를 삭제합니다.
}

 

void Model::ReadMesh()
{
    // 저장된 Mesh 데이터 파일 경로 생성
    string file = "Models/Meshes/" + name + ".mesh";

    // BinaryReader를 사용하여 Mesh 데이터 파일을 열고 읽기 위해 파일을 엽니다.
    BinaryReader* reader = new BinaryReader(file);

    // 파일 열기에 실패한 경우, assert 함수를 사용하여 오류를 처리합니다.
    // assert하는게 더 찾기 쉽고 안전하기 때문,
    if (reader->IsFailed()) assert(false);

    // Mesh 데이터의 개수를 파일에서 읽어옵니다.
    // 데이터 개수를 알아야 돌릴수있음.
    UINT size = reader->UInt();
    // 파일은 기입된 순서대로 읽어야한다.
  
    // Mesh 데이터를 저장할 meshes 벡터를 사전 예약합니다.
    meshes.reserve(size);

    // Mesh 데이터 개수만큼 반복하여 Mesh 데이터를 읽어옵니다.
    FOR(size)
    {
        // Mesh의 이름을 파일에서 읽어옵니다.
        string meshName = reader->String();

        // ModelMesh 클래스의 인스턴스를 생성하고 Mesh의 이름을 설정합니다.
        ModelMesh* mesh = new ModelMesh(meshName);

        // Mesh에 해당하는 Material을 materials 벡터에서 찾아 설정합니다.
        mesh->SetMaterial(materials[reader->UInt()]);

        // Mesh의 정점 (vertices) 개수를 파일에서 읽어옵니다.
        UINT vertexCount = reader->UInt();

        // 정점 데이터를 저장할 배열을 생성합니다.
        ModelVertex* vertices = new ModelVertex[vertexCount];

        // 파일에서 정점 데이터를 읽어와 배열에 저장합니다.
        reader->Byte((void**)&vertices, sizeof(ModelVertex) * vertexCount);

        // Mesh의 인덱스 (indices) 개수를 파일에서 읽어옵니다.
        UINT indexCount = reader->UInt();

        // 인덱스 데이터를 저장할 배열을 생성합니다.
        UINT* indices = new UINT[indexCount];

        // 파일에서 인덱스 데이터를 읽어와 배열에 저장합니다.
        reader->Byte((void**)&indices, sizeof(UINT) * indexCount);

        // Mesh를 생성하고 정점과 인덱스 데이터를 설정합니다.
        mesh->CreateMesh(vertices, vertexCount, indices, indexCount);

        // meshes 벡터에 Mesh 객체를 추가합니다.
        meshes.push_back(mesh);
    }

    // 사용이 끝난 BinaryReader를 삭제합니다.
    delete reader;
}

 

위의 Model데이터를 사용하기 위해 Material와 Mesh를 읽어오는 코드를 작성한다.

 

전에 만들었던 Material와 Mesh를 저장하는 함수와 같은순서대로 데이터를 읽어준다.

 

항상 저장한 데이터값의 순서대로 읽어야 한다.

 

윈도우 ReadFileAPI가 그렇게 동작하기 때문.

 

위의 머티리얼과 메시를 읽는 함수는 같은방식으로 동작한다.


 

모델의 메쉬를 저장하는 함수를 생각하기전에 메쉬가 트리형태로 저장된다는 사실을 먼저 기억해야한다.

 

메쉬 = 대부분 골반으로 시작해서 상체 하체가 나뉘며, 상체에서 목, 어깨 팔 순으로

하체에서 양다리로 양다리에서 발목까지 나아간다.

대충 위와같은 순서대로 이어지며 트리를 그린다.

 

그래서 각 메쉬당 가지는 Vertex를 정해서 정의해야한다.

#pragma once

class ModelMesh
{
public:
	ModelMesh(string name);
	~ModelMesh();

	void Render();
	
	void CreateMesh(void* vertices, UINT vertexCount,
		void* indices, UINT indexCount);

	void SetMaterial(Material* material) { this->material = material; }
	// 모델에 있는것을 참조받을것이다.

private:
	string name;

	Material* material;
	Mesh<ModelVertex>* mesh;
};

위와같은 식으로 Mesh를 생성하고 Material을 생성하고 따로 각부위별로 렌더하는 식으로 이루어진다.

ModelMesh::ModelMesh(string name) : name(name)
{
}

ModelMesh::~ModelMesh()
{
	delete mesh;
}

void ModelMesh::Render()
{
	material->Set();
	mesh->Draw();
}

void ModelMesh::CreateMesh(void* vertices, UINT vertexCount, void* indices, UINT indexCount)
{
    // Mesh 클래스의 인스턴스를 생성합니다.
    mesh = new Mesh<ModelVertex>();

    // Mesh의 정점 (vertices) 벡터를 할당하고 크기를 설정합니다.
    // 속도를 위해 미리 사이즈설정
    mesh->GetVertices().resize(vertexCount);

    // void 포인터로 전달된 정점 데이터를 복사하여 Mesh의 정점 벡터에 넣습니다.
    // 메모리cpy가 일반적인 데이터기입방식보다 더 빠르다.
    memcpy(mesh->GetVertices().data(), vertices, sizeof(ModelVertex) * vertexCount);

    // Mesh의 인덱스 (indices) 벡터를 할당하고 크기를 설정합니다.
    mesh->GetIndices().resize(indexCount);

    // void 포인터로 전달된 인덱스 데이터를 복사하여 Mesh의 인덱스 벡터에 넣습니다.
    memcpy(mesh->GetIndices().data(), indices, sizeof(UINT) * indexCount);

    // Mesh를 생성하는 함수를 호출하여 정점 및 인덱스 데이터로 Mesh를 초기화합니다.
    mesh->CreateMesh();
}

각 Mesh는 필요한 데이터별로 하나하나 정의되어 기입된다.

 

memcpy를 사용하는 이유는 주석에 달아놓은걸 읽으면 알 수 있듯이 효율성때문,

위 코드를 사용하면 대충 위와같이 모델링을 뽑아올 수 있다.

728x90