본문 바로가기

DirectX11 3D/기본 문법

<Directx11 3D> 39 - Model Material Rendering + Write File

Assimp 구조

 

 

필요한 개념


- Material은 Xml로 파일로 저장한다. 직접 외부에서 수정가능 하도록한다.

 

- Material(Color+ Texture)로 이루어져 있다.

 

<Color 값>

Color Ambient;
Color Diffuse;
Color Specular;
Color Emissive;

 

<텍스쳐 파일 경로>

string DiffuseFile;
string SpecularFile;
string NormalFile;

 

- *Material 텍스쳐 경로 문제

case 1) 파일명이 들어오지 않는다면 임의의 파일을 써줘야 한다(Tank가 그렇다.) -> .material texture 경로에 넣어줘야함
case 2) 내장 텍스쳐가 있다면
    case 2-1) 배열의 이미지가 y축은 없고(0인경우, 높이가 없다.) x축만 가지고 있는경우 -> 1자로 쭉 들어가 있음
    case 2-2) x,y축을 가지고 있는 경우
case 3) 내장 텍스쳐가 없다면(파일이 따로 있다.)

 

 

 

 

 

 

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);

public:
	// 디자인의 방법, 모델에 따라 Material파일을 임의로 수정할 필요가 생김
	// 직접 수정해야 하기에 Xml로 저장한다.
	// 2번째인자 : 덮어쓸지 여부(파일있으면 덮어씀), 만약 임의로 수정해서
	// 덮어씌우면 안되면 false이다.
	void ExportMaterial(wstring savePath, bool bOverWrite = true);

private:
	void ReadMaterialData();
	void WriteMaterialData(wstring savePath);
	string WriteTexture(string saveFolder, string file);

private:
	// 모델 파일 주소
	wstring file;

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

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

 

 

 

 

 

 

 

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);
}

void Converter::ExportMaterial(wstring savePath, bool bOverWrite)
{
	// 임의로 .material을 주었지만, 실제 우리가 사용하는 것은 xml이다.
	savePath = L"../../_Textures/" + savePath + L".material";

	// 덮어씌우지 않는다면
	if (bOverWrite == false)
	{
		// 파일이 존재 한다면 수행하지 않음
		if (Path::ExistFile(savePath) == true)
			return;
	}

	ReadMaterialData();
	WriteMaterialData(savePath);
}

void Converter::ReadMaterialData()
{
	for (UINT i = 0; i < scene->mNumMaterials; ++i)
	{
		// Scene에 메터리얼 정보가 배열로 저장되어있다.
		aiMaterial* srcMaterial = scene->mMaterials[i];
		asMaterial* material = new asMaterial();

		material->Name = srcMaterial->GetName().C_Str();

		// 색을 불러온다.
		// aiColor3D : r,g,b만 있다.
		aiColor3D color;

		// 매터리얼은 키, 값으로 구성되어있다. 키를 이용해 각 정보를 가져온다.
		// 1번째 인자 : 앰비언트 키
		// 함수에 들어가보면 2개로 이루어져있는 인자 함수가 없다.
		// AI_MATKEY_COLOR_AMBIENT는 3개로 구성되어있는 인자다.
		// AI_MATKEY_COLOR_AMBIENT 이 키에 대한 컬러값이 color 변수에 넘어감
		srcMaterial->Get(AI_MATKEY_COLOR_AMBIENT, color);
		// 빛은 반 투명이 없어서 1.0f으로 고정시키고 사용
		// 빛은 반 투명이 없으므로 강도로 대체해서 사용하는 경우가 많다.(어차피 a는 불투명(1.0f)이어서)
		material->Ambient = Color(color.r, color.g, color.b, 1.0f);

		srcMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, color);
		material->Diffuse = Color(color.r, color.g, color.b, 1.0f);

		srcMaterial->Get(AI_MATKEY_COLOR_SPECULAR, color);
		material->Specular = Color(color.r, color.g, color.b, 1.0f);

		// AI_MATKEY_SHININESS : SHININESS는 Specular의 강도이다.
		// specular에 alpha값에 강도를 넣어놓는다.
		srcMaterial->Get(AI_MATKEY_SHININESS, material->Specular.a);

		srcMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, color);
		material->Emissive = Color(color.r, color.g, color.b, 1.0f);


		// 텍스쳐를 불러온다.
		aiString file;

		// 텍스쳐는 경로명으로 넘어온다.
		// 1번째 인자(키)에 해당하는 경로명이 file 변수에 리턴된다.
		srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &file);
		material->DiffuseFile = file.C_Str();

		srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &file);
		material->SpecularFile = file.C_Str();

		srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &file);
		material->NormalFile = file.C_Str();

		materials.push_back(material);
	}
}

void Converter::WriteMaterialData(wstring savePath)
{
	// 디버깅해서 보면 각 모델의 material 정보 중 텍스쳐 경로를 보면
	// 어떤거는 파일이름만 되어있고, 어떤거는 없고 다 다르다.
	// 케이스가 많아서 따로 처리를 해줘야한다.

	//GetFileName-확장자 포함 파일명을 가져온다.
	//GetFileNameWithoutExtexsion-확장자 제외 이름만 가져온다.
	string folder = String::ToString(Path::GetDirectoryName(savePath));
	string file = String::ToString(Path::GetFileName(savePath));

	// 폴더를 만든다.
	Path::CreateFolders(folder);

	Xml::XMLDocument* document = new Xml::XMLDocument();

	Xml::XMLDeclaration* decl = document->NewDeclaration();
	document->LinkEndChild(decl);

	// XML 형식으로 쓰겠다라는 문구
	Xml::XMLElement* root = document->NewElement("Materials");
	root->SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
	root->SetAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
	document->LinkEndChild(root);

	for (asMaterial* material : materials)
	{
		// 대문자 XML::은 MS에서 제공하는 XML인데 우리는 tinyXMl 사용

		// Material 노드 만듬
		Xml::XMLElement* node = document->NewElement("Material");
		// Materials 노드의 하위 노드로 붙음
		root->LinkEndChild(node);

		// ex) Tank는 Material이 2개니까, 2개로 하위노드로 붙어서 나옴
		Xml::XMLElement* element = nullptr;

		// material 이름 저장
		element = document->NewElement("Name");
		// SetAttribue()을 써도 되지만, Attribute 는 <> 안에 속성으로 들어가고
		// SetText()를 쓰면 <> 이 사이에 들어간다. <>
		element->SetText(material->Name.c_str());
		// node의 자식으로 들어감
		node->LinkEndChild(element);


		// 텍스쳐 정보 저장
		element = document->NewElement("DiffuseFile");

		// 경로가 다 다르게 나온다. 임의의 경로 텍스쳐 경로로 바꿔줌
		// 이미지 파일을 저장함
		// 원래 있던 경로명을 다 제거하고 파일명만 쓸 것이다.
		element->SetText(WriteTexture(folder, material->DiffuseFile).c_str());
		node->LinkEndChild(element);

		element = document->NewElement("SpecularFile");
		element->SetText(WriteTexture(folder, material->SpecularFile).c_str());
		node->LinkEndChild(element);
        
        
		element = document->NewElement("NormalFile");
		element->SetText(WriteTexture(folder, material->NormalFile).c_str());
        node->LinkEndChild(element); 

		// Color 저장
		element = document->NewElement("Ambient");
		element->SetAttribute("R", material->Ambient.r);
		element->SetAttribute("G", material->Ambient.g);
		element->SetAttribute("B", material->Ambient.b);
		element->SetAttribute("A", material->Ambient.a);
		node->LinkEndChild(element);

		element = document->NewElement("Diffuse");
		element->SetAttribute("R", material->Diffuse.r);
		element->SetAttribute("G", material->Diffuse.g);
		element->SetAttribute("B", material->Diffuse.b);
		element->SetAttribute("A", material->Diffuse.a);
		node->LinkEndChild(element);

		element = document->NewElement("Specular");
		element->SetAttribute("R", material->Specular.r);
		element->SetAttribute("G", material->Specular.g);
		element->SetAttribute("B", material->Specular.b);
		element->SetAttribute("A", material->Specular.a);
		node->LinkEndChild(element);

		element = document->NewElement("Emissive");
		element->SetAttribute("R", material->Emissive.r);
		element->SetAttribute("G", material->Emissive.g);
		element->SetAttribute("B", material->Emissive.b);
		element->SetAttribute("A", material->Emissive.a);
		node->LinkEndChild(element);

		SafeDelete(material);
	}

	// 정보들을 Xml에 담는다.
	document->SaveFile((folder + file).c_str());
	SafeDelete(document);
}

string Converter::WriteTexture(string saveFolder, string file)
{
	// 이미지 파일을 생성을 하든, 복사를 하든 할 것임

	// case 1)
	// 파일명이 들어오지 않는다면 임의의 파일을 써줘야 한다.
	// 이 경우는 실제 Material 파일을 열어서 수정해야 하는 부분이다.
    // Tank가 그렇다.
	if (file.length() < 0) return "";

	string fileName = Path::GetFileName(file);
	
	// GetEmbeddedTexture() : 이 경로 명에 내장 텍스쳐가 있다면 내장 텍스쳐를 리턴받고
	// 없다면 null을 리턴함
	// fbx 파일에는 텍스쳐 파일이나, 다른 기타 파일들이 모두 합쳐져서 압축되어 들어가 있다.
	const aiTexture* texture = scene->GetEmbeddedTexture(file.c_str());

	// case 2) 내장 텍스쳐가 있다면
	// 내장 텍스쳐가 있다는 얘기(kachujin(.fbx)은 내장 텍스쳐가 있다.)
	string path = "";
	if (texture != nullptr)
	{
		// 배열의 이미지는 x,y축을 가지고 있다.

		// 저장하려는 경로 + 파일명
		path = saveFolder + fileName;

		// case 2-1) 배열의 이미지가 y축은 없고(0인경우, 높이가 없다.) x축만 가지고 있는경우
		// ex) kachujin
		if (texture->mHeight < 1)
		{
			// 텍스쳐의 높이가 0인경우
			// - 데이터가 한줄로 바이트 파일(png) 형식으로 써져 있다.
			// 즉, 그대로 저장하면 이미지 파일이 된다.

			// 텍스쳐 크기만큼 써버린다.
			BinaryWriter w;
			w.Open(String::ToWString(path));
			// pcData : 이미지 파일 순서대로, 바이트 대로 저장되어 있다.
			// pcData는 tga면 tga 순서대로, png면 png순서대로 저장되어있다. 그대로 쓰면됨
			// Byte 파일로 저장
			w.Byte(texture->pcData, texture->mWidth);
			w.Close();
		}

		// case 2-2) x,y축을 가지고 있는 경우
		// 높이가 있어서 텍스쳐 파일을 만들어서 씀
		// 현재까지 이런 파일을 보지는 못했지만
		// 이론상 있는 파일이다.
		else
		{
			D3D11_TEXTURE2D_DESC destDesc;
			ZeroMemory(&destDesc, sizeof(D3D11_TEXTURE2D_DESC));
			destDesc.Width = texture->mWidth;
			destDesc.Height = texture->mHeight;
			destDesc.MipLevels = 1;
			destDesc.ArraySize = 1;
			destDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
			destDesc.SampleDesc.Count = 1;
			destDesc.SampleDesc.Quality = 0;
			destDesc.Usage = D3D11_USAGE_IMMUTABLE;

			D3D11_SUBRESOURCE_DATA subResource = { 0 };
			subResource.pSysMem = texture->pcData;


			ID3D11Texture2D* dest;

			HRESULT hr;
			hr = D3D::GetDevice()->CreateTexture2D(&destDesc, &subResource, &dest);
			assert(SUCCEEDED(hr));

			D3DX11SaveTextureToFileA(D3D::GetDC(), dest, D3DX11_IFF_PNG, saveFolder.c_str());
		}
	}

	// case 3) 내장 텍스쳐가 없다면(파일이 따로 있다.)
	// B787은 fbx지만 따로 texture 파일이 존재
	// 모델 폴더에 파일이 있다면 파일을 복사해주고 경로를 맞도록 변경
	// 아니라면 빈 문자열을 리턴함
	else
	{
		string directory = Path::GetDirectoryName(String::ToString(this->file));
		string origin = directory + file;
		// /로 바꿔줌
		String::Replace(&origin, "\\", "/");

		// 파일이 없다면 빈문자열 리턴
		if (Path::ExistFile(origin) == false)
			return "";

		// 있다면 그 경로에서 파일을 copy
		path = saveFolder + fileName;
		// 3번째 인자는 FALSE 옵션은 파일이 있어도 덮어씌우는 옵션
		// save할 경로로 파일을 복사
		CopyFileA(origin.c_str(), path.c_str(), FALSE);

		// _Textures 문자열 제거, 경로만 쓰면 우리는 자동으로 받도록 처리해서
		String::Replace(&path, "../../_Textures/", "");
	}

	// 어차피 saveFolder경로가 texture 경로, texture 경로에 있는 파일을 부를 거라서
	return Path::GetFileName(path);
}

 

 

 

Types.h


더보기
#pragma once
#include "stdafx.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;
	int BoneIndex;

	aiMesh* Mesh;

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

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


struct asMaterial
{
	// Mesh가 가지고 있는 MaterialName과 이름이 동일하게 세팅함
	string Name;

	// Color 정보
	Color Ambient;
	Color Diffuse;
	Color Specular;
	Color Emissive;

	// 우리가 실제로 보여질 텍스쳐 파일
	// 텍스쳐 파일 경로
	string DiffuseFile;
	string SpecularFile;
	string NormalFile;

	// 나머지 조명배울때 쓸거라서 나중에 설명
};

 

 

Model.h


더보기
#pragma once
#define MAX_MODEL_TRANSFORMS 250

// Model을 부르는 코드

class ModelBone;
class ModelMesh;

class Model
{
public:
	// 원래는 모델 부를때 VertexTextureNormal까지 하면 되는데
	// 애니메이션할때 또 바꿔줘야되고 이래서.. 처음부터
	// VertexTextureNormalTangentBlend으로 한다.
	typedef VertexTextureNormalTangentBlend ModelVertex;

public:
	Model();
	~Model();

	UINT BoneCount() { return bones.size(); }
	vector<ModelBone*>& Bones() { return bones; }
	// 본의 번호로 얻기
	ModelBone* BoneByIndex(UINT index) { return bones[index]; }
	// 본의 이름으로 얻기
	ModelBone* BoneByName(wstring name);

	UINT MeshCount() { return meshes.size(); }
	vector<ModelMesh*>& Meshes() { return meshes; }
	ModelMesh* MeshByIndex(UINT index) { return meshes[index]; }
	ModelMesh* MeshByName(wstring name);

	UINT MaterialCount() { return materials.size(); }
	vector<Material*>& Materials() { return materials; }
	Material* MaterialByIndex(UINT index) { return materials[index]; }
	Material* MaterialByName(wstring name);

public:
	void ReadMesh(wstring file);
	void ReadMaterial(wstring file);

private:
	void BindBone();
	void BindMesh();

private:
	ModelBone* root;
	vector<ModelBone*> bones;
	vector<ModelMesh*> meshes;
	vector<Material*> materials;
};

 

 

 

 

 

 

 

Model.Cpp


더보기
#include "Framework.h"
#include "Model.h"
#include "Utilities/BinaryFile.h"
#include "Utilities/Xml.h"

Model::Model()
{
}

Model::~Model()
{
	for (ModelBone* bone : bones)
		SafeDelete(bone);

	for (ModelMesh* mesh : meshes)
		SafeDelete(mesh);

	for (Material* material : materials)
		SafeDelete(material);
}

ModelBone* Model::BoneByName(wstring name)
{
	for (ModelBone* bone : bones)
	{
		if (bone->Name() == name)
			return bone;
	}

	return NULL;
}

ModelMesh* Model::MeshByName(wstring name)
{
	for (ModelMesh* mesh : meshes)
	{
		if (mesh->Name() == name)
			return mesh;
	}

	return NULL;
}

Material* Model::MaterialByName(wstring name)
{
	for (Material* material : materials)
	{
		if (material->Name() == name)
			return material;
	}

	return NULL;
}

void Model::ReadMesh(wstring file)
{
	file = L"../../_Models/" + file + L".mesh";


	// 크기를 맞춰서 읽는다.(바이너리 파일이니까)
	BinaryReader* r = new BinaryReader();
	r->Open(file);

	UINT count = 0;
	count = r->UInt();
	for (UINT i = 0; i < count; i++)
	{
		ModelBone* bone = new ModelBone();
		bone->index = r->Int();
		bone->name = String::ToWString(r->String());
		bone->parentIndex = r->Int();
		bone->transform = r->Matrix();

		bones.push_back(bone);
	}

	count = r->UInt();
	for (UINT i = 0; i < count; i++)
	{
		ModelMesh* mesh = new ModelMesh();

		mesh->name = String::ToWString(r->String());
		mesh->boneIndex = r->Int();
		mesh->materialName = String::ToWString(r->String());

		//VertexData
		{
			UINT count = r->UInt();

			vector<Model::ModelVertex> vertices;
			// assign : 미리 데이터의 크기를 잡아놓는다.
			vertices.assign(count, Model::ModelVertex());

			void* ptr = (void*)&(vertices[0]);
			r->Byte(&ptr, sizeof(Model::ModelVertex) * count);


			mesh->vertices = new Model::ModelVertex[count];
			mesh->vertexCount = count;
			copy
			(
				vertices.begin(), vertices.end(),
				stdext::checked_array_iterator<Model::ModelVertex*>(mesh->vertices, count)
			);
		}

		//IndexData
		{
			UINT count = r->UInt();

			vector<UINT> indices;
			indices.assign(count, UINT());

			void* ptr = (void*)&(indices[0]);
			r->Byte(&ptr, sizeof(UINT) * count);


			mesh->indices = new UINT[count];
			mesh->indexCount = count;
			copy
			(
				indices.begin(), indices.end(),
				stdext::checked_array_iterator<UINT*>(mesh->indices, count)
			);
		}

		meshes.push_back(mesh);
	}

	r->Close();
	SafeDelete(r);

	BindBone();
}

void Model::BindBone()
{
	// 0번이 루트
	root = bones[0];

	for (ModelBone* bone : bones)
	{
		// parentIndex가 -1이면 Root이다.

		if (bone->parentIndex > -1)
		{
			bone->parent = bones[bone->parentIndex];
			// 자식으로 추가해줌
			bone->parent->childs.push_back(bone);
		}
		else
		{
			bone->parent = nullptr;
		}
	}
}

void Model::BindMesh()
{
	for (ModelMesh* mesh : meshes)
	{
		// 배열에 있던거 가져다 연결해줌
		mesh->bone = bones[mesh->boneIndex];

		mesh->Binding(this);
	}
}

void Model::ReadMaterial(wstring file)
{
	file = L"../../_Textures/" + file + L".material";

	Xml::XMLDocument* document = new Xml::XMLDocument();
	Xml::XMLError error = document->LoadFile(String::ToString(file).c_str());
	assert(error == Xml::XML_SUCCESS);

	Xml::XMLElement* root = document->FirstChildElement();
	Xml::XMLElement* materialNode = root->FirstChildElement();

	do
	{
		Material* material = new Material();


		Xml::XMLElement* node = NULL;

		node = materialNode->FirstChildElement();
		material->Name(String::ToWString(node->GetText()));

		wstring directory = Path::GetDirectoryName(file);
		String::Replace(&directory, L"../../_Textures", L"");


		wstring texture = L"";

		node = node->NextSiblingElement();
		texture = String::ToWString(node->GetText());
		if (texture.length() > 0)
			material->DiffuseMap(directory + texture);

		node = node->NextSiblingElement();
		texture = String::ToWString(node->GetText());
		if (texture.length() > 0)
			material->SpecularMap(directory + texture);

		node = node->NextSiblingElement();
		texture = String::ToWString(node->GetText());
		if (texture.length() > 0)
			material->NormalMap(directory + texture);


		Color color;

		node = node->NextSiblingElement();
		color.r = node->FloatAttribute("R");
		color.g = node->FloatAttribute("G");
		color.b = node->FloatAttribute("B");
		color.a = node->FloatAttribute("A");
		material->Ambient(color);

		node = node->NextSiblingElement();
		color.r = node->FloatAttribute("R");
		color.g = node->FloatAttribute("G");
		color.b = node->FloatAttribute("B");
		color.a = node->FloatAttribute("A");
		material->Diffuse(color);

		node = node->NextSiblingElement();
		color.r = node->FloatAttribute("R");
		color.g = node->FloatAttribute("G");
		color.b = node->FloatAttribute("B");
		color.a = node->FloatAttribute("A");
		material->Specular(color);

		node = node->NextSiblingElement();
		color.r = node->FloatAttribute("R");
		color.g = node->FloatAttribute("G");
		color.b = node->FloatAttribute("B");
		color.a = node->FloatAttribute("A");
		material->Emissive(color);

		materials.push_back(material);

		materialNode = materialNode->NextSiblingElement();
	} while (materialNode != NULL);

	BindMesh();
}

 

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);
}

 

 

ModelRender.h


더보기
#pragma once

// Model 렌더링 코드가 들어감

class ModelRender
{
public:
	ModelRender(Shader* shader);
	~ModelRender();

	void Update();
	void Render();

public:
	void ReadMesh(wstring file);
	void ReadMaterial(wstring file);
	
	// 모델의 위치
	Transform* GetTransform() { return transform; }
	Model* GetModel() { return model; }

	// 메쉬들을 돌아가면서 Pass를 일괄적으로 설정한다.
	void Pass(UINT pass);

	// Transform 업데이트, 2번째 인자 : bone 행렬 전체
	void UpdateTransform(ModelBone* bone = nullptr, Matrix& matrix = Matrix());

private:
	// 본을 회전하기 위해 만든 함수
	void UpdateBones(ModelBone* bone, Matrix& matrix);

private:
	// 데이터를 읽었느냐
	bool bRead = false;

	Shader* shader;

	// Model Wrapper class
	Model* model;
	Transform* transform;

	Matrix transforms[MAX_MODEL_TRANSFORMS];
};

 

 

 

ModelRender.Cpp


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

ModelRender::ModelRender(Shader* shader)
	: shader(shader)
{
	model = new Model();
	transform = new Transform(shader);
}

ModelRender::~ModelRender()
{
	SafeDelete(model);
	SafeDelete(transform);
}

void ModelRender::Update()
{
	if (bRead == true)
	{
		bRead = false;
		
		for (ModelMesh* mesh : model->Meshes())
			mesh->SetShader(shader);

		UpdateTransform();
	}

	for (ModelMesh* mesh : model->Meshes())
		mesh->Update();
}

void ModelRender::Render()
{
	// 렌더링의 기준은 Model Mesh이며, 모델이 그려질 transform을 세팅해준다.
	for (ModelMesh* mesh : model->Meshes())
	{
		// 메쉬가 어느 위치에서 그려질지
		mesh->SetTransform(transform);
		mesh->Render();
	}
}

void ModelRender::ReadMesh(wstring file)
{
	model->ReadMesh(file);
}

void ModelRender::ReadMaterial(wstring file)
{
	model->ReadMaterial(file);

	bRead = true;
}

void ModelRender::Pass(UINT pass)
{
	for (ModelMesh* mesh : model->Meshes())
		mesh->Pass(pass);
}

void ModelRender::UpdateTransform(ModelBone* bone, Matrix& matrix)
{
	if (bone != nullptr)
		UpdateBones(bone, matrix);

	for (UINT i = 0; i < model->BoneCount(); i++)
	{
		// 해당인덱스의 본을 가져옴
		ModelBone* bone = model->BoneByIndex(i);

		// Bone에 있는 Transform들이 전부 복사됨
		transforms[i] = bone->Transform();
	}

	// ModelMesh에 Transforms 넘겨줌, ModelMesh가 boneBuffer를 Cbuffer로 넘겨주기 때문
	for (ModelMesh* mesh : model->Meshes())
		mesh->Transforms(transforms);
}

void ModelRender::UpdateBones(ModelBone* bone, Matrix& matrix)
{
	// 본을 업데이트, 이동, 회전 등...
	Matrix temp = bone->Transform();

	// 원래 본을 기준으로 matrix 만큼 이동
	bone->Transform(temp * matrix);

	// 자식들도 이동
	for (ModelBone* child : bone->Childs())
		UpdateBones(child, matrix);
}

 

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->ReadMaterial(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->ReadMaterial(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->ReadMaterial(L"Tank/Tank");
	tank->GetTransform()->Position(20, 0, 0);
}

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

 

 

.fx


더보기
#include "00_Global.fx"

float3 Direction = float3(-1, -1, 1);

struct VertexModel
{
    float4 Position : Position;
    float2 Uv : Uv;
    float3 Normal : Normal;
    float3 Tangent : Tangent;
    float4 BlendIndices : BlendIndices;
    float4 BlendWeights : BlendWeights;
};

#define MAX_MODEL_TRANSFORMS 250

cbuffer CB_Bone
{
    matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
    
    uint BoneIndex;
    
    // CBuffer 전체가 배열이 되지 않는한, padding은 안 넣어줘도 된다.
};

struct VertexOutput
{
    // SV_Position -> position0번으로 취급됨
    float4 Position : SV_Position;
    float3 Normal : Normal;
    float2 Uv : Uv;
};

VertexOutput VS(VertexTextureNormal input)
{
    VertexOutput output;
    
    World = mul(BoneTransforms[BoneIndex], World);
    
    // ModelMesh의 Transform이 넘어오나, Bone의 위치로 옮긴 다음, World로 옮겨야함
    output.Position = WorldPosition(input.Position);
    output.Position = ViewProjection(output.Position);
    
    output.Normal = WorldNormal(input.Normal);
    output.Uv = input.Uv;
    
    return output;
}

float4 PS(VertexOutput input) : SV_Target
{    
    // 음영 넣기 위해
    float NdotL = dot(normalize(input.Normal), -Direction);
    
    return DiffuseMap.Sample(LinearSampler, input.Uv) * NdotL;
}

technique11 T0
{
    P_VP(P0, VS, PS)
    P_RS_VP(P1, FillMode_WireFrame, VS, PS)
}

 

 

 

 

 

결과