후... 이걸 완성하려고 달리다보니 글 쓸 시간도 없었다.
boost 라이브러리를 활용해 파일로 클래스를 입출력 할 수 있게 되었기 때문에, 다음과 같은 구조를 만들어 주었다.
class FileIO
{
public:
template<typename Ty_> static void Save(Ty_ t, std::string filename);
template<typename Ty_> static void Load(Ty_& data, std::string filename);
};
template<typename Ty_>
inline void FileIO::Save(Ty_ t, std::string filename)
{
std::ofstream out(filename);
boost::archive::text_oarchive oArchive(out);
oArchive << t;
}
template<typename Ty_>
inline void FileIO::Load(Ty_& data, std::string filename)
{
std::ifstream in(filename);
boost::archive::text_iarchive iArchive(in);
iArchive >> data;
}
먼저 위와 같이 파일을 읽기, 쓰기 할 수 있도록 만들어 주었다.
다음으로, FMaterial(File Material) 클래스를 다음과 같이 만들어 주었다.
class FMaterial
{
friend class Material;
private:
friend class boost::serialization::access;
template<class Archive>
void serialize(Archive& ar, const unsigned int version)
{
ar& topology;
ar& vs;
ar& ps;
ar& datas;
}
public:
enum class Topology
{
Triangle,
LineList,
};
enum Type
{
Int,
Float,
Vector1 = Float,
Vector2,
Vector3,
Vector4,
Color,
Texture2D,
};
template<typename Ty_> void Add(Type type, Ty_ data);
template<> void Add(Type type, std::string data);
template<> void Add(Type type, const char* data);
FMaterial() = default;
~FMaterial() = default;
public:
Topology topology = Topology::Triangle;
std::string vs, ps;
std::vector<std::pair<Type, std::vector<char>>> datas;
};
template<typename Ty_>
inline void FMaterial::Add(Type type, Ty_ data)
{
datas.push_back(
{
type, PrimiviteConverter_2Char::Convert(std::move(data))
}
);
}
template<>
inline void FMaterial::Add(Type type, std::string data)
{
std::vector<char> v(data.begin(), data.end());
v.push_back('\0');
datas.push_back(
{
type, std::move(v)
}
);
}
template<>
inline void FMaterial::Add(Type type, const char* data)
{
Add(type, std::string(data));
}
Material 정보에는 해당 Mesh의 Topolgy를 어떻게 할 것인가, 그리고 vertexShader, PixelShader의 이름을 멤버로 가지고, 그외에 들어가는 정보는 모두다 vector<char>로 저장하고 있다. 저번에 Vertex 클래스를 만들면서 사용했던 기법이다. 데이터 공간에 강제로 값을 복사해서 사용하는 식이다. 이렇게 하면 타입이 어떤 타입인지 상관없이 모두 우겨넣을 수가 있다.
중간에 보면 PrimitiveConverter_2Char 라는 클래스가 있는데, 단순히 값을 vector<char>로 변환하여 리턴해주는 기능을 담당하고 있다.
이렇게 Material을 저장할 수 있게 되었지만, 이를 다시 Renderer에서 사용하려면 매 프레임마다 어떤 셰이더를 쓰는지, 어떤 텍스쳐를 바인딩해야하는지를 일일이 이 파일의 string을 보고 검색해서 갖고 온다는 건 너무 속도가 느려지므로, Material 내부에서 필요한 소스들의 포인터를 들고 있도록 만드는 게 중요하다.
그래서 이러한 작업을 하기 위해 엔진 내부에서 사용할 Material 클래스를 다시 따로 제작해주었다.
class Material
{
friend class Renderer;
public:
enum class Topology
{
Triangle,
LineList,
} topology;
enum class Type
{
Int,
Float,
Double,
Vector1,
Vector2,
Vector3,
Vector4,
Color,
Texture2D,
};
template <Type type> struct Map;
template <> struct Map<Type::Int> {
using DataType = int;
};
template <> struct Map<Type::Float> {
using DataType = float;
};
template <> struct Map<Type::Double> {
using DataType = double;
};
template <> struct Map<Type::Vector1> {
using DataType = Vector;
};
template <> struct Map<Type::Vector2> {
using DataType = Vector2;
};
template <> struct Map<Type::Vector3> {
using DataType = Vector3;
};
template <> struct Map<Type::Vector4> {
using DataType = Vector4;
};
template <> struct Map<Type::Color> {
using DataType = Vector4;
};
template <> struct Map<Type::Texture2D> {
using DataType = Texture;
};
Material(class FileManager* fileManager, FMaterial& fMat);
Material() = default;
virtual ~Material() = default;
void Bind();
private:
Shader::Vertex* vs;
Shader::Pixel* ps;
std::vector<std::pair<Type, std::vector<char>>> datas;
};
이 Material도 데이터 저장을 vector<char>로 하는 건 똑같다. 이 녀석은 정의 파트가 핵심이다.
Material::Material(class FileManager* fileManager, FMaterial& fMat)
{
topology = (Topology)fMat.topology;
fileManager->Get(fMat.vs, vs);
fileManager->Get(fMat.ps, ps);
for (const auto& it : fMat.datas)
{
switch (it.first)
{
case FMaterial::Texture2D:
{
std::string path = it.second.data();
Texture* texture = fileManager->Get(path, texture);
datas.push_back({ Type::Texture2D, PrimiviteConverter_2Char::Convert(texture) });
break;
}
case FMaterial::Color:
{
std::vector<void*> v;
datas.push_back({ Type::Color, it.second });
}
default:
break;
}
}
}
위와 같이 FMaterial에서 정보를 읽어내서 실제 fileManager가 들고 있는 Texture의 포인터를 저장하는 방식이다. 포인터 뿐만 아니라 구조체나 float같은 자료형 데이터도 vector<char>로 충분히 저장할 수 있기 때문에 모든 타입의 데이터를 저장할 수 있게 된다.
다만 여기서 실수한 점이 있었는데, 포인터 타입의 데이터를 집어넣었을 경우 꺼낼 때 어떻게 타입 캐스팅을 하는가였다.
생각해보면 vector.data()를 하면 배열의 첫번째 주소를 리턴하는데, 이 주소값이 내가 원하는 타입의 데이터를 가리키는 포인터다! 라고 강제 형 변환을 통해 사용하는 방식이라는 걸 먼저 기억하자. 즉, Texture*로 강제 형 변환을 했을 경우 해당 데이터는 Texture를 가리키는 데이터가 된다. 당연히 오류가 날 수 밖에. 텍스처의 포인터의 포인터를 가리킨다고 해주어야 해당 메모리가 가리키는 값이 Texture*가 된다. 참 기초적인 내용인데 생각치도 못하고 있었다.
void Material::Bind()
{
for (UINT i=0; i< datas.size(); ++i)
{
switch (datas[i].first)
{
case Type::Texture2D:
{
Texture* t = *reinterpret_cast<Texture**>(datas[i].second.data());
t->Bind();
break;
}
case Type::Color:
break;
default:
break;
}
}
}
이렇게 바인딩을 해주는 식이다.
이렇게 렌더링에 필요한 정보들을 파일로 저장하고 불러올 수 있게 되었다. 다음으로는 Renderer에서 어떻게 정보를 받아와서 그리는가인데, 그냥 심플하게 했다.
class RenderComponent : public Component
{
public:
RenderComponent(std::string name, class GameObject* g);
public:
Mesh* pMesh = nullptr;
Material material;
protected:
void Update() override;
};
모든 렌더링 작업 컴포넌트의 부모 클래스를 제작해주고 이를 상속하도록 해주었다. 이때,
RenderComponent::RenderComponent(std::string name, GameObject* g)
:Component(name, g)
{
renderer.Add(this);
}
생성자에서 renderer에게 해당 컴포넌트를 추가해달라고 요청한다.
다음은 Renderer를 살펴보자.
private:
std::list<RenderComponent*> renderComponents;
멤버로 list를 갖고 있는데, 이는 RenderComponent는 Disable 상태가 되면 그리지 않기 때문에 출입이 원활해야 한다. 그래서 삭제와 삽입이 용이한 list를 사용해봤다. 사실 성능 테스트할 정도로 모델이 많지도 않아서 체감은 안된다.
그리고 이 RenderComponent 포인터를 가지고 와서 렌더링 작업을 수행하는 함수 Draw를 보자.
void Renderer::Draw(RenderComponent* rc)
{
if (!rc->pMesh) return;
switch (rc->material.topology)
{
case Material::Topology::Triangle:
gfx.context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
break;
case Material::Topology::LineList:
gfx.context.IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
break;
}
BindInputLayout(rc->material.vs, rc->pMesh);
if (rc->pMesh) rc->pMesh->Draw();
rc->material.vs->Bind();
rc->material.ps->Bind();
rc->material.Bind();
sampler->Bind();
CB_Transformation tr{ dx::XMMatrixTranspose(
rc->gameObject.transform.transformMatrix *
mainCamera->viewMatrix *
mainCamera->projectionMatrix)
};
cb_transform.data = tr;
cb_transform.Bind();
gfx.context.DrawIndexed(rc->pMesh->indicesCount, 0, 0);
}
심플하다. material에도 Bind 함수를 호출시켜서 Renderer가 직접 분석하지 않고 material이 분석해서 알아서 Bind 하도록 했다. 아직 추가는 안했지만 float, vector2 같은 타입들도 상수버퍼를 만들어서 거기에 집어넣어 주어야 할 것이다. 이는 나중에 셰이더와 연동하도록 개발하여서 자동으로 필요한 상수버퍼를 생성하도록 할 생각이다.
마지막으로 mesh 별로 mesh Renderer가 붙을 수 있도록 하기 위해 Model로부터 GameObject를 생성하는 함수를 만들었다.
GameObject* SceneManager::Add(Model* model)
{
GameObject* go = new GameObject("ai");
for (const auto& it : model->meshes)
{
auto mesh = new GameObject(it->name);
auto& meshrenderer = mesh->AddComponent<MeshRenderer>();
meshrenderer.pMesh = it.get();
int nName = it->name.rfind(".");
std::string materialName = it->name.substr(0, nName) + ".material";
FMaterial* fmat = Scene::fileManager->Get(materialName, fmat);
meshrenderer.material = Material(Scene::fileManager, *fmat);
go->AddChild(mesh);
}
Scene::currentScene->root->AddChild(go);
return go;
}
일단 임시로 Mesh의 이름을 필요한 텍스쳐의 이름으로 두고 그 이름에서 확장자를 material로 바꿔서 원하는 Material을 Mesh Renderer가 들고있도록 했다.
Time::deltatime 의 값을 보니 확실히 함수 객체를 생성하는 것 보다 많이 빨라진 거 같긴 하다. 굿굿.
'진행과정 기록 > GameEngine' 카테고리의 다른 글
20200225 Rendering 과정 개선(1) (0) | 2020.02.25 |
---|---|
20200224 Grid (0) | 2020.02.24 |
20200222 Camera UI (0) | 2020.02.22 |
20200221 (0) | 2020.02.21 |
20200219 (0) | 2020.02.19 |