DirectX

49. Light, Material_Material

devRiripong 2024. 2. 16.
반응형

Material을 하고

이어서 NormalMapping을 해본다.

 

Material은 뭘까?

쉐이더를 만들었는데 그때마다 등장했다.

물체마다 들고 있는 여러 잡동사니 정보들이다.

지금까지는 Mesh, Texture를 MeshRenderer에 세팅하는 식으로 작업을 했다.

Material이 뭔지 생각해 보면, 쉐이더를 이용해 물체를 그린다 할 때, 그 부분에 넘겨주는 인자라고 볼 수 있다.

 

1. Material 클래스에서 Shader에 넘겨줄 인자들을 Shader에 전달하는 기능을 구현하기

 

LightingDemo::Update()에서

MaterialDesc desc;
desc.ambient = Vec4(0.2f);
desc.diffuse = Vec4(1.f);
desc.specular = Vec4(1.f);
//desc.emissive = Color(0.3f, 0.f, 0.f, 0.5f);

RENDER->PushMaterialData(desc);
_obj->Update();

ambient, duiffuse, specular, emissive 같은 색상들에 대한 옵션도 넘겨주고 있긴 하지만 나중에 가면 각기 사용하는 쉐이더에 넘겨주는 인자들 뿐만 아니라 Texture 같은 것도 Material의 인자라 볼 수 있다.

 

지금은 하다 보니까 MeshRenderer에 Texture를 들고 있게끔 하드코딩이 되어 있는데

_shader->GetSRV("DiffuseMap")->SetResource(_texture->GetComPtr().Get());

이 부분이 사실상 Material로 빠져가지고 통합이 되어야 한다.

Normal mapping을 하면 normalMap을 사용할 것이기 때문에 이 부분을 먼저 챙긴다고 보면 된다.

 

Material 개념은 2D를 할 때도 알아본 적이 있다.

그 정도로 상용 엔진 사용할 때 재질이라고 별도의 개념이라 생각하기 쉬운데

사실 이 Material 이 결국 쉐이더에 넘기는 인자의 모음에 불과하다 할 수 있다.

 

그 부분을 복원시켜 본다.

 

1) Material 클래스 생성하기 

Engine/00. Engine/;Resource 필터에 새 클래스를 추가한다.

Material을 이름으로 하고, ResourceBase를 기본클래스로 상속받게 설정을 한다.

 

ResourceBase를 상속받고 있다.

그래서 필요한 정보가 여럿 있다.

#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; // Material의 각기 색상들이 들어있다.

	shared_ptr<Shader> _shader; 
	shared_ptr<Texture> _diffuseMap; 
	shared_ptr<Texture> _normalMap; 
	shared_ptr<Texture> _specularMap; 
	// 텍스쳐를 여기에 세팅한 다음에 
	// 그 쉐이더에 따라서 각기 접근할 수 있는 애들을 캐싱해서 
	// 나중에 material을 이용해 세팅을 할 때 그걸 이용해서 바로 하자가 컨셉이다.
	ComPtr<ID3DX11EffectShaderResourceVariable> _diffuseEffectBuffer; 
	ComPtr<ID3DX11EffectShaderResourceVariable> _normalEffectBuffer; 
	ComPtr<ID3DX11EffectShaderResourceVariable> _specularEffectBuffer; 
};

함수들을 정의하면

#include "pch.h"
#include "Material.h"

Material::Material() : Super(ResourceType::Material)
{

}

Material::~Material()
{
}

void Material::SetShader(shared_ptr<Shader> shader)
{
	_shader = shader; 

	_diffuseEffectBuffer = shader->GetSRV("DiffuseMap");
	_normalEffectBuffer = shader->GetSRV("NormalMap");
	_specularEffectBuffer = shader->GetSRV("SpecularMap");
}

여기까지가 기본적인 material 세팅이다.

별도의 클래스를 빼서 편리하게 쉐이더에 넘겨줘야 하는 인자들을 관리하고 있는 새로운 클래스를 만들었다.

 

2) Material::Update에서 Shader에 material, diffuseMap, normalMap, specularMap 데이터 넣어주기

만약 이 material을 Update 한다는 건 Render라는 함수를 만들어도 동일할 것이다. 매 프레임마다 Shader에다가 뭔가 제출을 해야 한다라고 하면 그 부분을 여기서 처리하게끔 묶어 주도록 한다.

 

상용엔진을 공부할 때 Material이 특이하게 생각될 수도 있지만 따지고 보면 이런 식으로 쉐이더 쪽에다 데이터를 밀어 넣을 때 그러니까 GPU 쪽에다가 데이터를 밀어 넣는 작업에 불과하다는 걸 강조하고 있다.

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

 

3) ResourceManager::GetResourceType에서 T가 Material인 경우를 추가해서 _resources행렬에 Material 타입도 key 값으로 쓸 수 있게 하기

material이 만약에 생성이 됐다면, Resource를 사용해서, ResouceManager를 사용해서 이거를 접근하는 게 일반적일 것이다.

ResourceManager에 Material이 빠져있으니

ResourceManager.h에

class Material;

전방선언을 하고,

ResourceManager::GetResourceType에서

if (std::is_same_v<T, Material>)
		return ResourceType::Material;

이걸 추가한다.

 

이걸 하는 이유는 material은 material끼리 키가 안 겹치게끔 이렇게 맵에다가

private:
	using KeyObjMap = map<wstring/*key*/, shared_ptr<ResourceBase>>;
	array<KeyObjMap, RESOURCE_TYPE_COUNT> _resources;

뭉쳐서 관리하게끔 설정해 놓은 것이다.

 

Engine 프로젝트를 빌드한다.

 

2. MeshRenderer에서 인자들을 Material로 묶어서 꽂아주기

이런식으로 Material을 세팅을 할 것이다가 관건이었고, 실질적으로 material은 누가 들고 있는 것이냐.

Resource이지 Component가 아니다. 그러다 보니 물체에 붙이는 개념은 아니다.

사용 엔진을 보면 MeshRenderer에서 쉐이더에다가 넘겨줘야 하는 그런 온갖 잡동사니 인자들을 material로 묶어 가지고 관리한다는 걸 쉽게 확인할 수 있다.

MeshRenderer에다가 꽂아주는 식으로 작업을 해주면 된다.

 

1) Material 변수 선언 하고, Material에 포함되어 필요 없어진 shader, texture 과련 요소들 주석처리 하기

MeshRenderer.h에

shared_ptr<Material> _material;

를 추가해 주고,

사실상 _texutre, _shader는 통합이 되어 가지고, 이 _material 안으로 들어간다고 보면 된다.

//shared_ptr<Texture> _texture;
//shared_ptr<Shader> _shader;

주석처리를 한다.

SetTexture, SetShader를 삭제하는 게 맞겠지만 그러면 위쪽 코드들이 난리 나기 때문에 일단 legacy로 내버려둔다.

// Legacy
void SetTexture(shared_ptr<Texture> texture) { }
void SetShader(shared_ptr<Shader> shader) { }

한꺼번에 정리할 때 지우도록 한다.

2) SetMaterial 추가하기 

void SetMaterial(shared_ptr<Material> material) { _material = material; }

SetMaterial을 추가한다.

 

Material의 용도를 기억하면 된다.

Shader에 넘겨주는 인자들에 불과했다.

 

3) MeshRenderer::Update에서 texture와  shader를 material로 묶어서 처리하게 하기

MeshRenderer::Update를 복붙 하고, 하나를 주석 처리 하고, 새로 바뀐 버전을 만든다.

#include "Material.h"

를 추가하고,

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

한 번 정리가 된 것이다.

 

쉐이더가 Material에 끼워 있어야 하는데, 그건 당연하다.

Material이 혼자 동작하는 게 아니라 Material이란 게 쉐이더에 인자를 넘기는 것이기 때문에 사실상 당연하다고 생각할 수 있다.

 

Engine 프로젝트를 빌드한다.

 

3. MaterialDemo 클래스에서 shader, texture, lighting을 Material을 활용해 세팅하기

1) MaterialDemo 클래스 생성하고 Main에 세팅하기

MaterialDemo를 이어서 만들어 볼 것인데

material을 이용해서 뭔가를 만들어 본다가 핵심이기 때문에 어떤 코드에 끼워 넣어도 상관없다. 16. LightingDemo 클래스를 복붙 해서 이름을 17. MaterialDemo로 한다. 그리고 Client/Game/Week2에 넣고 실습을 이어간다.

코드를 MaterialDemo에 맞게 수정하고,

 

Main으로 가서 세팅한다.

#include "17. MaterialDemo.h"
desc.app = make_shared<MaterialDemo>(); // 실행 단위

 

2) MaterialDemo::Init에서 material에 shader, texture, ambient, diffuse를 세팅하고 _resources에 넣어주기

#include "Material.h"

하고,

 

MaterialDemo::Init에서 먼저 Material부터 만든다.

void MaterialDemo::Init()
{
	RESOURCES->Init();
	_shader = make_shared<Shader>(L"13. Lighting.fx");

	// Material
	{
		shared_ptr<Material> material = make_shared<Material>();
		{
			material->SetShader(_shader); 
		}
		{
			auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
			material->SetDiffuseMap(texture); 
		}

		MaterialDesc& desc = material->GetMaterialDesc(); 
		desc.ambient = Vec4(1.f); 
		desc.diffuse = Vec4(1.f); 

		RESOURCES->Add(L"Veigar", material); 
	}

이렇게 해주면 앞으로 material을 누군가가 필요로 할 때 똑같은 이름을 꺼내 가지고 사용할 수 있게 되는 거다.

 

3) Shader세팅은 Material로 옮겼으니 MaterialDemo::Init 의 //Object에서 SetShader 호출하는 부분을 삭제하기

// Object
	_obj = make_shared<GameObject>(); 
	_obj->GetOrAddTransform(); 
	_obj->AddComponent(make_shared<MeshRenderer>());
	{
		_obj->GetMeshRenderer()->SetShader(_shader);
	}
	{
		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); 
	}

//Object에도 Shader를 세팅하는 부분이 있지만 shader는 material에 들어있는 개념이다 보니까

{
		_obj->GetMeshRenderer()->SetShader(_shader);
}

이 부분은 삭제한다.

 

4) Texture세팅도 Material로 옮겼으니 MaterialDemo::Init 의 //Object에서 MeshRenderer에 texture를 세팅하는 부분을 삭제하고 ReourceManager를 이용해 Material을 불러오고, _obj의 MeshRenderer에 Material을 세팅하기

	{
		auto texture = RESOURCES->Load<Texture>(L"Veigar", L"..\\\\Resources\\\\Textures\\\\veigar.jpg");
		_obj->GetMeshRenderer()->SetTexture(texture); 
	}

texture도 여기서 세팅하는 게 아닌데 regacy라서 함수를 내버려둔 거라 error가 안 난 것이다. 사실 material로 세팅해야 한다.

{
		auto material = RESOURCES->Get<Material>(L"Veigar");
		_obj->GetMeshRenderer()->SetMaterial(material); 
}

이게 기본적인 실제 엔진의 방식이다.

 

	// 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"Veigar");
		_obj->GetMeshRenderer()->SetMaterial(material);
	}

 

5) Obj2도 마찬가지로 MeshRenderer에 Material을 세팅하기 

// Object2도 마찬가지로 해준다.

쉐이더 세팅하는 부분을 삭제하고,

// Object2
	_obj2 = make_shared<GameObject>();
	_obj2->GetOrAddTransform()->SetPosition(Vec3{ 0.5f, 0.f, 2.f });
	_obj2->AddComponent(make_shared<MeshRenderer>());
	{
		auto mesh = RESOURCES->Get<Mesh>(L"Cube");
		_obj2->GetMeshRenderer()->SetMesh(mesh);
	}
	{
		auto material = RESOURCES->Get<Material>(L"Veigar");
		//MaterialDesc& desc = material->GetMaterialDesc();
		//desc.ambient = Vec4(1.f, 0.f, 0.f, 1.f);
		//desc.diffuse = Vec4(0.2f);

		_obj2->GetMeshRenderer()->SetMaterial(material);
	}

다른 옵션으로 세팅을 할 수 있고 안 할 수도 있다..

 

4. 테스트해보고 Material의 다른 옵션을 obj 각각에 적용하는 방법 찾기

실행하면

 

1) _obj2의 material의 desc를 수정하기 -  다른 obj 전부 적용된다. 

material사용할 때 조심할 부분이 있는데

두 번째 물체는 조금 더 다른 방식으로 보이고 싶다고 가정을 해본다.

{
    auto material = RESOURCES->Get<Material>(L"Veigar");
    MaterialDesc& desc = material->GetMaterialDesc();
    desc.ambient = Vec4(0.f);
    desc.diffuse = Vec4(0.f);

    _obj2->GetMeshRenderer()->SetMaterial(material);
}

이렇게 ambient와 diffuse를 0으로 한다면 Cube는 안 보이게 될 것이다.

 

실행을 해보면

큐브뿐만 아니라 스피어도 안보이게 된다.

 

왜 그런 걸까?

두 번째 material을 고쳤는데 material이 먹통이 되어서 모두가 보이지 않게 되었다.

Resource를 작업할 때 모든 리소스가 이런 식이다.

유니티에서도 이런 부분을 조심해야 한다.

 

리소스 특징이 뭘까?

컴포넌트, 리소스를 구분하는 가장 큰 특징이 뭘까?

메모리에서 하나만 올려놓고, 그걸 재정의 해서 쓰는 거다.

누군가가 다르게 동작하고 싶다고 옵션을 바꾸기 시작하면

ResourceManager를 통해 얻어 온 게 원본이 같다.

같은 원본을 고쳤기 때문에 MaterialDemo:;Init에서 세팅한

MaterialDesc& desc = material->GetMaterialDesc(); 
desc.ambient = Vec4(1.f); 
desc.diffuse = Vec4(1.f); 

RESOURCES->Add(L"Veigar", material);

이 정보는 날아가고,

_obj2에서의 정보로 갱신이 된 것이다.

 

2) Material::Clone 함수를 만들어서, 수정한 material이 하나의 obj에 적용되게 하기 

만약에 obj2 만 다르게 하고 싶었다면

엔진마다 방법이 다르지만

근본적으로 별도의 material을 만들어야 한다.

Material을 그대로 사용하는 개념이 아니라 Clone 해가지고 별도의 복사본을 만들어서 사용해야 한다.

 

ResourceBase.h에 가면 원래는 Clone이라는 가상함수가 있는 게 일반적이다.

Material.h에서 복제품이지만 원본에는 영향을 안주는 버전을 만들고 싶다면

shared_ptr<Material> Clone();

Clone이라는 복사하는 함수를 만들어서 들고 있었던 모든 값들을 동일하게 들고 있게끔 만들어주는 게 필요하다.

std::shared_ptr<Material> Material::Clone()
{
	shared_ptr<Material> material = make_shared<Material>();

	material->_desc = _desc;
	material->_shader = _shader;
	material->_diffuseMap = _diffuseMap;
	material->_normalMap = _normalMap;
	material->_specularMap = _specularMap;
	material->_diffuseEffectBuffer = _diffuseEffectBuffer;
	material->_normalEffectBuffer = _normalEffectBuffer;
	material->_specularEffectBuffer = _specularEffectBuffer;

	return material;
}

material을 만들어 줄 거지만, 원래 들고 있었던 모든 정보들을 다 동일하게 넣어 줄 것이고,

다음에 Clone 한 것을 고친다면 원본에는 더 이상 영향을 안 주게 된다.

 

Engine프로젝트를 rebuild 한다.

 

3) Clone()을 이용해 만든 material을 MeshRenderer에 세팅하기

변경한 게 모든 애들에게 영향을 주는 게 목적이 아니라면

MaterialDemo::Init의 obj2부분에서

{
	auto material = RESOURCES->Get<Material>(L"Veigar")->Clone();
	MaterialDesc& desc = material->GetMaterialDesc();
	desc.ambient = Vec4(0.f);
	desc.diffuse = Vec4(0.f);

	_obj2->GetMeshRenderer()->SetMaterial(material);
}

이렇게 →Clone()을 해주고 실행해 보면

 

4) 각각 다른 오브젝트에 다른 material의 설정값이 적용되었는지 확인하기

Cube에만 Material의 변경 사항이 적용이 된 것을 알 수 있다.

 

Material이 두 개가 된 것이다.

 

 

5. 맺음말

 

유니티에서. mat 파일을 열어보면 text로 이루어진 것을 볼 수 있다.

 

Material은 정보를 넘기는 용도로 사용하는 것이다.

리소스다 보니까 공용으로 재사용하는 거다 보니까 하나를 바뀌면 다 같이 바뀐다.

가 결론이다.

반응형

'DirectX' 카테고리의 다른 글

51. Light, Material_버그수정(카메라 좌표)  (0) 2024.02.17
50. Light, Material_Normal Mapping  (0) 2024.02.17
48. Light, Material_Light 통합  (0) 2024.02.15
47. Light, Material_Emissive  (0) 2024.02.14
46. Light, Material_Specular  (0) 2024.02.13

댓글