모델 #3 모델 띄우기
지난시간까진 Assimp라이브러리를 이용해서 fbx파일을 읽고 필요한 정보만 머티리얼정보는 .xml파일로, 그외의 메쉬정보(정점, 인덱스, 이름, 계층구조)는 .mesh정보로 저장하는 작업을 AssimpTool프로젝트를 만들어 작업했다.
이제 이렇게 만든 파일을 다시 AssimpTool프로젝트에서 읽어다가 쓰는 작업을 해보자.
우선 Engine프로젝트에서 우리가 직접만든 파일을 쓰기전에는 텍스쳐를 로드하고 GeometryHelper로 만든 메쉬를 ResourceBase를 상속받은 Mesh에서 인덱스정보와 버텍스 정보를 갖고있게하고, 리소스 매니저에서 이를 가져와 오브젝트에 메쉬를 설정하도록 해서 쓰고 있었다.
// 18.NormalMappingDemo.cpp
// Object
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform();
_obj->AddComponent(make_shared<MeshRenderer>());
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
}
{
auto material = RESOURCES->Get<Material>(L"Leather");
_obj->GetMeshRenderer()->SetMaterial(material);
}
그리고 오브젝트가 들고있는 컴포넌트중 하나인 MeshRenderer에서 Update시(Material정보도 이때 가져와) 그려지곤 했다.
하지만 우리는 이 Material, Mesh정보를 쓰지 않고 파일로부터 읽어들인 계층정보도 있는 메쉬와 머티리얼 정보를 사용할 것이다. 그래서 새로운 Model, ModelMesh,Component상속받은 ModelRenderer클래스들을 만들어 Assimp라이브러리로 파싱해 원하는 구조로 저장했던 fbx파일을 읽어들여 사용하고자 한다.
Engine프로젝트의 Resource필터에 Model필터를 추가하고 Model,ModelMesh클래스를 추가했다.
커스텀포맷으로 저장한 파일들을 읽어다 쓸 수 있도록 Engie프로젝트에 추가했다. (실제로 호출할것은 AssimpTool프로젝트인데) Mesh클래스가 이미 ResourceBase를 상속받아 만들어져 있는데 AssimpTool로 만든 메쉬는 이와는 조금 다르지만 수정하기 어려우므로 아예 Model클래스를 Model필터를 만들어서 만들었다.
우선 ResourceBase를 상속받아 만들었던 Mesh 클래스를 대체할 ModelMesh클래스이다. ResourceBase를 상속받진않지만 똑같이 지오메트리정보, 버텍스버퍼, 인덱스 버퍼를 들고있고 이 메쉬에 연결될 머티리얼 정보와 이 메쉬의 번호(boneIndex), 그리고 계층구조 정보(bone)를 들고있게 했다.
// ModelMesh.h
#pragma once
struct ModelBone
{
wstring name;
int32 index;
int32 parentIndex;
shared_ptr<ModelBone> parent; // Cache
Matrix transform;
vector<shared_ptr<ModelBone>> children; // Cache
};
struct ModelMesh
{
void CreateBuffers();
wstring name;
// Mesh
shared_ptr<Geometry<ModelVertexType>> geometry = make_shared<Geometry<ModelVertexType>>();
shared_ptr<VertexBuffer> vertexBuffer;
shared_ptr<IndexBuffer> indexBuffer;
// Material
wstring materialName = L"";
shared_ptr<Material> material; // Cache
// Bones
int32 boneIndex;
shared_ptr<ModelBone> bone; // Cache;
};
그냥 쓰고 asMesh만 조금 수정해서 써보자. asBone을 ModelBone클래스로 바꾼다.
ModelMesh클래스에 material 로 캐싱해놓고 없으면 리소스 매니저에서 가져다가 캐싱해서 쓴다.
그 다음은 Model클래스 이다. Model은 오브젝트가 MeshRenderer대신 ModelRenderer컴포넌트를 들고 그릴때 필요한 정보이다. 뼈대(계층구조)와 메쉬들, 머티리얼들의 정보를 갖고 있을 것이다. Model객체로 파일들을 로드해서 메모리에 올린다음에 오브젝트에 세팅해준다.
// Model.h
#pragma once
struct ModelBone;
struct ModelMesh;
class Model : public enable_shared_from_this<Model>
{
public:
Model();
~Model();
public:
void ReadMaterial(wstring filename);
void ReadModel(wstring filename);
uint32 GetMaterialCount() { return static_cast<uint32>(_materials.size()); }
vector<shared_ptr<Material>>& GetMaterials() { return _materials; }
shared_ptr<Material> GetMaterialByIndex(uint32 index) { return _materials[index]; }
shared_ptr<Material> GetMaterialByName(const wstring& name);
uint32 GetMeshCount() { return static_cast<uint32>(_meshes.size()); }
vector<shared_ptr<ModelMesh>>& GetMeshes() { return _meshes; }
shared_ptr<ModelMesh> GetMeshByIndex(uint32 index) { return _meshes[index]; }
shared_ptr<ModelMesh> GetMeshByName(const wstring& name);
uint32 GetBoneCount() { return static_cast<uint32>(_bones.size()); }
vector<shared_ptr<ModelBone>>& GetBones() { return _bones; }
shared_ptr<ModelBone> GetBoneByIndex(uint32 index) { return (index < 0 || index >= _bones.size() ? nullptr : _bones[index]); }
shared_ptr<ModelBone> GetBoneByName(const wstring& name);
private:
void BindCacheInfo();
private:
wstring _modelPath = L"../Resources/Models/";
wstring _texturePath = L"../Resources/Textures/";
private:
shared_ptr<ModelBone> _root;
vector<shared_ptr<Material>> _materials;
vector<shared_ptr<ModelBone>> _bones;
vector<shared_ptr<ModelMesh>> _meshes;
};
// Model.cpp
// ...
void Model::ReadMaterial(wstring filename)
{
wstring fullPath = _texturePath + filename + L".xml";
auto parentPath = filesystem::path(fullPath).parent_path();
tinyxml2::XMLDocument* document = new tinyxml2::XMLDocument();
tinyxml2::XMLError error = document->LoadFile(Utils::ToString(fullPath).c_str());
assert(error == tinyxml2::XML_SUCCESS);
tinyxml2::XMLElement* root = document->FirstChildElement();
tinyxml2::XMLElement* materialNode = root->FirstChildElement();
while (materialNode)
{
shared_ptr<Material> material = make_shared<Material>();
tinyxml2::XMLElement* node = nullptr;
node = materialNode->FirstChildElement();
material->SetName(Utils::ToWString(node->GetText()));
// Diffuse Texture
node = node->NextSiblingElement();
if (node->GetText())
{
wstring textureStr = Utils::ToWString(node->GetText());
if (textureStr.length() > 0)
{
auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
material->SetDiffuseMap(texture);
}
}
// Specular Texture
node = node->NextSiblingElement();
if (node->GetText())
{
wstring texture = Utils::ToWString(node->GetText());
if (texture.length() > 0)
{
wstring textureStr = Utils::ToWString(node->GetText());
if (textureStr.length() > 0)
{
auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
material->SetSpecularMap(texture);
}
}
}
// Normal Texture
node = node->NextSiblingElement();
if (node->GetText())
{
wstring textureStr = Utils::ToWString(node->GetText());
if (textureStr.length() > 0)
{
auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring());
material->SetNormalMap(texture);
}
}
// Ambient
{
node = node->NextSiblingElement();
Color color;
color.x = node->FloatAttribute("R");
color.y = node->FloatAttribute("G");
color.z = node->FloatAttribute("B");
color.w = node->FloatAttribute("A");
material->GetMaterialDesc().ambient = color;
}
// Diffuse
{
node = node->NextSiblingElement();
Color color;
color.x = node->FloatAttribute("R");
color.y = node->FloatAttribute("G");
color.z = node->FloatAttribute("B");
color.w = node->FloatAttribute("A");
material->GetMaterialDesc().diffuse = color;
}
// Specular
{
node = node->NextSiblingElement();
Color color;
color.x = node->FloatAttribute("R");
color.y = node->FloatAttribute("G");
color.z = node->FloatAttribute("B");
color.w = node->FloatAttribute("A");
material->GetMaterialDesc().specular = color;
}
// Emissive
{
node = node->NextSiblingElement();
Color color;
color.x = node->FloatAttribute("R");
color.y = node->FloatAttribute("G");
color.z = node->FloatAttribute("B");
color.w = node->FloatAttribute("A");
material->GetMaterialDesc().emissive = color;
}
_materials.push_back(material);
// Next Material
materialNode = materialNode->NextSiblingElement();
}
BindCacheInfo();
}
void Model::ReadModel(wstring filename)
{
wstring fullPath = _modelPath + filename + L".mesh";
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(fullPath, FileMode::Read);
// Bones
{
const uint32 count = file->Read<uint32>();
for (uint32 i = 0; i < count; i++)
{
shared_ptr<ModelBone> bone = make_shared<ModelBone>();
bone->index = file->Read<int32>();
bone->name = Utils::ToWString(file->Read<string>());
bone->parentIndex = file->Read<int32>();
bone->transform = file->Read<Matrix>();
_bones.push_back(bone);
}
}
// Mesh
{
const uint32 count = file->Read<uint32>();
for (uint32 i = 0; i < count; i++)
{
shared_ptr<ModelMesh> mesh = make_shared<ModelMesh>();
mesh->name = Utils::ToWString(file->Read<string>());
mesh->boneIndex = file->Read<int32>();
// Material
mesh->materialName = Utils::ToWString(file->Read<string>());
//VertexData
{
const uint32 count = file->Read<uint32>();
vector<ModelVertexType> vertices;
vertices.resize(count);
void* data = vertices.data();
file->Read(&data, sizeof(ModelVertexType) * count);
mesh->geometry->AddVertices(vertices);
}
//IndexData
{
const uint32 count = file->Read<uint32>();
vector<uint32> indices;
indices.resize(count);
void* data = indices.data();
file->Read(&data, sizeof(uint32) * count);
mesh->geometry->AddIndices(indices);
}
mesh->CreateBuffers();
_meshes.push_back(mesh);
}
}
BindCacheInfo();
}
void Model::BindCacheInfo()
{
// Mesh에 Material 캐싱
for (const auto& mesh : _meshes)
{
// 이미 찾았으면 스킵
if (mesh->material != nullptr)
continue;
mesh->material = GetMaterialByName(mesh->materialName);
}
// Mesh에 Bone 캐싱
for (const auto& mesh : _meshes)
{
// 이미 찾았으면 스킵
if (mesh->bone != nullptr)
continue;
mesh->bone = GetBoneByIndex(mesh->boneIndex);
}
// Bone 계층 정보 채우기
if (_root == nullptr && _bones.size() > 0)
{
_root = _bones[0];
for (const auto& bone : _bones)
{
if (bone->parentIndex >= 0)
{
bone->parent = _bones[bone->parentIndex];
bone->parent->children.push_back(bone);
}
else
{
bone->parent = nullptr;
}
}
}
}
Model에는 들고있는 모델메쉬정보 벡터, 모델메쉬정보들의 상속관계를 나타낼 본즈 벡터,그리고 같이 쓸 머티리얼 벡터를 갖고있게 했다.
우선 ReadMateral에서 읽어드린 .xml파일에서 asMaterial 대신 ResourceBase를 상속받은 Material을 쓰고자 한다. tinyxml2를 사용해 파싱해서 Material객체마다 텍스쳐맵이나 조명값들을 설정하고 _materials에 추가했다.
다만 ReadModel에서는 계층구조에 관한정보를 먼저 모두 읽어드린 다음에 메쉬 정보를 본격적으로 읽어드린다.
메쉬렌더러 컴포넌트 대신 ModelRenderer를 사용해 그리도록 Component를 상속해서 만들었다. 이 컴포넌트를 GameObject가 들고있는것이다.
// ModelRenderer.h
#pragma once
#include "Component.h"
class Model;
class Shader;
class Material;
class ModelRenderer : public Component
{
using Super = Component;
public:
ModelRenderer(shared_ptr<Shader> shader);
virtual ~ModelRenderer();
virtual void Update() override;
void SetModel(shared_ptr<Model> model);
void SetPass(uint8 pass) { _pass = pass; }
private:
shared_ptr<Shader> _shader;
uint8 _pass = 0;
shared_ptr<Model> _model;
};
// ModelRenderer.cpp
void ModelRenderer::Update()
{
if (_model == nullptr)
return;
// Bones
BoneDesc boneDesc;
const uint32 boneCount = _model->GetBoneCount();
for (uint32 i = 0; i < boneCount; i++)
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
boneDesc.transforms[i] = bone->transform;
}
RENDER->PushBoneData(boneDesc);
// Transform
auto world = GetTransform()->GetWorldMatrix();
RENDER->PushTransformData(TransformDesc{ world });
const auto& meshes = _model->GetMeshes();
for (auto& mesh : meshes)
{
if (mesh->material)
mesh->material->Update();
// BoneIndex
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
uint32 stride = mesh->vertexBuffer->GetStride();
uint32 offset = mesh->vertexBuffer->GetOffset();
DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
}
}
void ModelRenderer::SetModel(shared_ptr<Model> model)
{
_model = model;
const auto& materials = _model->GetMaterials();
for (auto& material : materials)
{
material->SetShader(_shader);
}
}
ModelRenderer의 Update에서 모델이 있는지 확인해서 그린다. GameObject에 이 ModelRenderer컴포넌트를 추가한다.
MeshRenderer와의 차이점은 메쉬렌더러는 하나의 메쉬, 하나의 머티리얼을 그렸다면 ModelRenderer에서는 여러개 가지고 있는 메쉬를 순회하면서 메쉬에 맞는 머티리얼을 가져다가 그린다.
AssimpTool프로젝트에 StaticMeshDemo클래스를 추가했다. IExecute를 상속받았고, Client프로젝트에서 했던 것처럼 AssimpTool프로젝트의 진입점이 있는 곳에 GameDesc타입의 앱에 StaticMeshDemo를 연결해줬다.
그리고 StaticMeshDemo는 아래와 같이 작성하였다.
// StaticDemo.h
#include "IExecute.h"
class StaticMeshDemo : public IExecute
{
public:
void Init() override;
void Update() override;
void Render() override;
void CreateTower();
void CreateTank();
private:
shared_ptr<Shader> _shader;
shared_ptr<GameObject> _obj;
shared_ptr<GameObject> _camera;
};
이렇게 IExecute를 상속받아서 Init, Update, Render를 만들고 연결할 쉐이더와 배치할 카메라, 그리고 게임오브젝트는 다를 샘플데모와 똑같다. 다만 CreateTower, CreateTank를 연결하고싶은 게임오브젝트에 따라 Init에서 호출하게 했다.
CreateTank의 소스코드는 아래와 같다.
// StaticMeshDemo.cpp
void StaticMeshDemo::CreateTank()
{
// CustomData -> Memory
shared_ptr<class Model> m1 = make_shared<Model>();
m1->ReadModel(L"Tank/Tank");
m1->ReadMaterial(L"Tank/Tank");
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 50));
_obj->GetOrAddTransform()->SetScale(Vec3(1.f));
_obj->AddComponent(make_shared<ModelRenderer>(_shader));
{
_obj->GetModelRenderer()->SetModel(m1);
_obj->GetModelRenderer()->SetPass(1);
}
}
Tower의 메시는 하나로 돼있었는데 Tank는 계층구조를 가지고 있어 얘를 로드해서 테스트해본다.
만약에 계층구조 없이 바로 로드하면
위와 같이 탱크의 모습은 볼 수 없고 하나로 뭉쳐져서 나온다. 오른쪽 사진처럼 유니티에서 Tank.fbx를 열어보면 메쉬끼리 계층구조를 가지고 있는데 bones의 정보를 활용하지 않고 모든 메쉬를 중앙에 배치해서 생긴 문제이다.
15.ModeDemol.fx새로운 쉐이더 파일에(Client프로젝트\Shaders에 넣어주었다.) 최대 50개 (MAX_MODEL_TRANSFORMS 개) 뼈대 정보를 받아주도록 만들어주고 지금 렌더링하고 있는 뼈대의 인덱스 정보를 BoneBuffer라는 이름으로 상수버퍼를 추가했다. 그리고 이 값들을 ModelRenderer에서 채워서 넘겨준다.
ModelRenderer의 Update에서
여기서 RenderManager의 PushBoneData를 호출하고 쉐이더에 직접 본인덱스를 전달한다.
RenderManager에도 같은 정보를 추가한다. ModelRenderer에서 호출할 PushBoneData도 만들어줬다. 상수버퍼 BoneBuffer를 채워준다.
그리고 StaticMeshDemodml Update에서 ModelRenderer의 Update가 호출되면 본정보, 트랜스폼정보 등이 쉐이더에 넘어가고 각 본 메쉬들을 돌아가면서 그리게 된다.
void ModelRenderer::Update()
{
if (_model == nullptr)
return;
// Bones
BoneDesc boneDesc;
const uint32 boneCount = _model->GetBoneCount();
for (uint32 i = 0; i < boneCount; i++)
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
boneDesc.transforms[i] = bone->transform;
}
RENDER->PushBoneData(boneDesc);
// Transform
auto world = GetTransform()->GetWorldMatrix();
RENDER->PushTransformData(TransformDesc{ world });
const auto& meshes = _model->GetMeshes();
for (auto& mesh : meshes)
{
if (mesh->material)
mesh->material->Update();
// BoneIndex
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
uint32 stride = mesh->vertexBuffer->GetStride();
uint32 offset = mesh->vertexBuffer->GetOffset();
DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
}
}
그리고 본들에 들어 있는 트랜스폼 좌표는 각 메쉬들의 직속 부모의 상대좌표이므로 이를 오브젝트 자체의(루트) 기준의 로컬 좌표로 바꿔주기위해 쉐이더에서 또 수정이 필요하다.
15.ModelDemo.fx를 수정한다.
그리고 본들에 들어 있는 트랜스폼 좌표는 각 메쉬들의 직속 부모의 상대좌표이므로 이를 오브젝트 자체의(루트) 기준의 로컬 좌표로 바꿔주기위해 쉐이더에서 또 수정이 필요하다.
15.ModelDemo.fx를 수정한다
BoneTransforms행렬 배열에는 각 부품이 로컬로 넘어가는 행렬이 들어 있는것이다.