Material
Material이란 무엇인가?
이전에 RenderManager에서 MaterialDesc타입을 정의해서 ambient, diffuse, specular값을 들고 있는데 각각의 오브젝트가 하나씩 들고 있도록 했다. 그리고 이 MaterialDesc를 값을 넣어 RenderManager클래스의 PushMaterialData로 상수버퍼에 넘겨주었다.
또 MeshRenderer컴포넌트의 Update에서는 메쉬와 텍스쳐를 이용해 그릴 수 있도록 쉐이더에 넘겨주기도 했었다.
=> 이 둘에서 공통적으로 볼 수 있는 것은 쉐이더에서 그리는데 필요한 값들을 넘겨주는 역할을 한다는 것이다.
*그리고 MeshRenderer컴포넌트에서 텍스쳐를 지금은 가지고있는데 사실은 이부분이 머티리얼로 빠지고 RenderManager에서 하고잇는 MaterialDesc를 채워주는 부분도 MeshRenderer컴포넌트에서 들고있어야한다.이를 수정해보자.
정리하면,
✨머티리얼이란 재질이라고 알려져 있지만 사실 쉐이더에 넘기는 인자의 모음에 불과하다✨
1. Material
ResourceBase클래스를 부모로 Material클래스를 만들었다. Engine프로젝트의 Resource필터하위에 두었다.
참고로 ResourceBase를 상속하고있는 다른 클래스들은 Texture, Mesh가 있었고 모두 싱글톤으로된 ResourceManager에서
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
_obj->GetMeshRenderer()->SetMesh(mesh);
auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\Resources\\Textures\\veigar.jpg");
_obj->GetMeshRenderer()->SetTexture(texture);
이런식으로 쓰이고 있었다.
Material클래스를 아래와 같이 작성하였다.
// Material.h
#pragma once
#include "ResourceBase.h"
class Material : public ResourceBase
{
using Super = ResourceBase;
public:
Material();
virtual ~Material();
shared_ptr<Shader> GetShader() { return _shader; }
MaterialDesc& GetMaterialDesc() { return _desc; }
shared_ptr<Texture> GetDiffuseMap() { return _diffuseMap; }
shared_ptr<Texture> GetNormalMap() { return _normalMap; }
shared_ptr<Texture> GetSpecularMap() { return _specularMap; }
void SetShader(shared_ptr<Shader> shader);
void SetDiffuseMap(shared_ptr<Texture> diffuseMap) { _diffuseMap = diffuseMap; }
void SetNormalMap(shared_ptr<Texture> normalMap) { _normalMap = normalMap; }
void SetSpecularMap(shared_ptr<Texture> specularMap) { _specularMap = specularMap; }
void Update();
shared_ptr<Material> Clone();
private:
//friend class MeshRenderer;
MaterialDesc _desc;
shared_ptr<Shader> _shader;
shared_ptr<Texture> _diffuseMap;
shared_ptr<Texture> _normalMap;
shared_ptr<Texture> _specularMap;
ComPtr<ID3DX11EffectShaderResourceVariable> _diffuseEffectBuffer;
ComPtr<ID3DX11EffectShaderResourceVariable> _normalEffectBuffer;
ComPtr<ID3DX11EffectShaderResourceVariable> _specularEffectBuffer;
};
Material클래스에서는 MeshRenderer 컴포넌트 클래스를 freind클래스로 만들어서 private영역에 접근할 수 있게 만들었다. 그리고 RenderManager에 정의했던 MaterialDesc 타입 변수를 하나 들고있게 해 쉐이더에 넘겨줄 오브젝트의 재질에 대한 정보를 가지고 있게 했다.
재질에 관한 것 뿐만아니라 쉐이더에 넘겨줘야하는 것들이 Material이라고 했는데 그럼 이 정보들을 받을 쉐이더는 무엇이어야할지, 그리고 쉐이더에 넘길 텍스쳐들(디퓨즈맵(그냥 입힐 텍스쳐), 노멀맵, 스페큘러맵(?))도 추가해줬다.
그리고 이들을 Effect11이랑 연동하므로 이펙트버퍼들을 각각 만들어줬다. ID3DX11EffectShaderResourceVariable 타입은 d3dx11effect.h에서 ID3D11ShaderResourceView타입의 쉐이더리소스뷰를 가지고 제어하는데 쓰도록 만든 것이다.
즉, 텍스쳐들을 가지고있게하고 이펙트버퍼에 텍스쳐들을 캐싱해서 쓰려는 것이다.
Material클래스의 구현부를 보면
우선 Material이 상속받고있는 ResourceBase클래스는 기본 생성자가 만들어지는 걸 막아뒀기 때문에(ResourceType을 받는 생성자 하나만 명시해둠) Material의 생성자에서 같이 만들어줬다.
Material::Material() : Super(ResourceType::Material)
{
}
또 Material::SetShader에서 모든 값들을 전달받을 shader를 인자로 받아서 세팅하고 쉐이더가 가지고 있는 쉐이더 리소스뷰를 할당해줬다.(쉐이더에서 쓰는 텍스쳐를 연결하는 것으로) 예를들어 00.Light.fx쉐이더 파일에서
Texture2D NormalMap;
이렇게 되어있는걸 _shader에 00.Light.fx파일을 들고있도록 할당하고 _shader->GetSRV("NormalMap");
이렇게 쉐이더리소스뷰로 가져오게 만들어 _normalmalEffectBuffer에 넣는것이다.
void Material::SetShader(shared_ptr<Shader> shader)
{
_shader = shader;
_diffuseEffectBuffer = shader->GetSRV("DiffuseMap");
_normalEffectBuffer = shader->GetSRV("NormalMap");
_specularEffectBuffer = shader->GetSRV("SpecularMap");
}
void Material::Update()
{
if (_shader == nullptr)
return;
RENDER->PushMaterialData(_desc);
if (_diffuseMap)
_diffuseEffectBuffer->SetResource(_diffuseMap->GetComPtr().Get());
if (_normalMap)
_normalEffectBuffer->SetResource(_normalMap->GetComPtr().Get());
if (_specularMap)
_specularEffectBuffer->SetResource(_specularMap->GetComPtr().Get());
}
MeshRenderer컴포넌트의 Update에서 호출될 Material의 Update함수도 만들어준다. 여기서 매 프레임 계산되는 쉐이더 연산에서 쓰이는 리소스들이 캐싱되어있다가 전달된다.
Clone함수도 만들었는데 이는 일종의 머티리얼의 마스터가 있고 복제품을 만들어 일부만 다른 값을 가지고 있고싶을때 값을 복사해서 새로운 Material객체를 만드는 함수이다.
2. ResourceManager
싱글톤으로된 ResourceManager에서 Texture, Mesh와 함께 Material도 들고 있을 수 있도록 수정한다.
// ResourceManager.h
class Shader;
class Texture;
class Mesh;
class Material;
class ResourceManager
{
DECLARE_SINGLE(ResourceManager);
// ...
}
// ...
template<typename T>
ResourceType ResourceManager::GetResourceType()
{
if (std::is_same_v<T, Texture>)
return ResourceType::Texture;
if (std::is_same_v<T, Mesh>)
return ResourceType::Mesh;
if (std::is_same_v<T, Material>)
return ResourceType::Material;
assert(false);
return ResourceType::None;
}
ResourceManager에서는 map으로 ResourceBase을 상속받은 타입의 리소스들을 관리하고있다.
3. MeshRenderer 컴포넌트
MeshRenderer컴포넌트는 모든 오브젝트들이 가지고 있는 컴포넌트로 컴포넌트들이 Update될때 호출되며 여기선 그려진다.
// MeshRenderer.cpp
//void MeshRenderer::Update()
//{
// if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
// return;
//
// _shader->GetSRV("DiffuseMap")->SetResource(_texture->GetComPtr().Get());
//
// auto world = GetTransform()->GetWorldMatrix();
// RENDER->PushTransformData(TransformDesc{ world });
//
// uint32 stride = _mesh->GetVertexBuffer()->GetStride();
// uint32 offset = _mesh->GetVertexBuffer()->GetOffset();
//
// DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
// DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
//
// _shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
//}
void MeshRenderer::Update()
{
if (_mesh == nullptr || _material == nullptr)
return;
auto shader = _material->GetShader();
if (shader == nullptr)
return;
_material->Update();
auto world = GetTransform()->GetWorldMatrix();
RENDER->PushTransformData(TransformDesc{ world });
uint32 stride = _mesh->GetVertexBuffer()->GetStride();
uint32 offset = _mesh->GetVertexBuffer()->GetOffset();
DC->IASetVertexBuffers(0, 1, _mesh->GetVertexBuffer()->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(_mesh->GetIndexBuffer()->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
shader->DrawIndexed(0, 0, _mesh->GetIndexBuffer()->GetCount(), 0, 0);
}
이 MeshRenderer컴포넌트 클래스의 Update함수도 위와 같았던걸 아래처럼 수정했다.
이전엔 MeshRenderer에서 직접 Texture와 Shader를 들고있던것을 Mesh와 Material을 들고있도록 바꾼 것이다.
이제 이 MeshRenderer컴포넌트를 들고있는 오브젝트에 쉐이더나 텍스쳐를 전달해주는 대신 메쉬와 머티리얼을 전달해 Update에서 그리게 할 것이다.
4. MaterialDemo클래스
Client프로젝트에 LightDemo클래스를 복붙해서 MaterialDemo클래스를 하나 만들었다.그리고 기존걸 아래처럼 바뀌었다.
Material객체를 Init에서 생성해서 ResourceManager가 포인터는 들고 있게 만들고, 함께 쓰일 쉐이더와 디퓨즈맵텍스쳐를 연결해준다. 그리고 방금 만든 Material은 ResourceManager인 RESOURCES에 Veigar라는 이름으로 들고있게도 했다. 우선 Material하나를 로드한것이라고 할 수 있다.
카메라 세팅은 LightDemo와 동일하며 오브젝트 객체를 만든뒤 컴포넌트와 메쉬를 추가하고 MaterialDemo에선 위에서 만든 Materail객체를 RESOURCES에서 가져와 붙여줬다.
두번째 오브젝트도 첫번째 오브젝트와 같은 디퓨즈맵을 갖고있게 하고 싶으나 MaterialDesc값만 Veigar Material에서 바꾸고 싶어서 클론해가지고 설정하였다.
즉, LightDemo클래스에선 LightDemo클래스 안의 오브젝트에 포함된 MeshRenderer컴포넌트에서 텍스쳐와 쉐이더를 들고있게 했는데 MaterialDemo클래스에선 Material객체를 생성해서 오브젝트에 포함된 MeshRenderer컴포넌트에 전달해 MeshRenderer의 Update에서 그릴때 이 Material이 들고 있는 값을 사용하게 한 것이다.
정리하면 Material클래스는 쉐이더에 넘겨줘야하는 인자들을 관리하는 클래스라고 하였다. 오브젝트의 MeshRenderer컴포넌트에 Material 오브젝트를 ResourceManager에서 가져와 SetMaterial로 넘겨줘 가리키게 하여,
오브젝트가 그려질때 MeshRenderer컴포넌트에서 가지고 있는 Material정보가 Update에서 불려와 처음 Init에서 설정된 쉐이더와 각종 텍스쳐맵을 쉐이더에 넘겨주어 그릴 정보들을 전달해주었다.
MaterialDemo를 실행하면 구조적으로 변화만 있었을 뿐 이전과 똑같은 결과를 보여준다.