DirectX

41. DirectX11 3D 입문_Mesh

devRiripong 2024. 2. 10.
반응형

실습을 편리하게 할 수 있게 Mesh들을 묶어서 관리하는 실습을 해본다.

 

1. 09. MeshDemo 생성하고 Main에서 세팅하기

탐색기에서 08. NormalDemo 클래스를 복붙 해서 09. MeshDemo로 이름을 바꾼다. 클래스를 MeshDemo에 맞게 수정하고,

솔루션 탐색기에서 Client/Game 필터에 넣는다.

 

Mesh란 무엇이고 어떻게 나중에 지금 흩어져 있는 아이들을 묶어서 관리할 것인가를 얘기할 것이다.

 

Main.cpp에서

#include "09. MeshDemo.h"

추가하고

 

WinMain에서

desc.app = make_shared<MeshDemo>(); // 실행 단위

이렇게 세팅한다.

 

셰이더는 바꿀 필요 없고,

09. MeshDemo.h의

// Object
	shared_ptr<Geometry<VertexTextureNormalData>> _geometry;
	shared_ptr<VertexBuffer> _vertexBuffer; 
	shared_ptr<IndexBuffer> _indexBuffer; 
	Matrix _world = Matrix::Identity;

이 부분을 깔끔하게 묶어서 관리하는 게 목표다.

 

2D에서 _geometry, _vertexBufferf, _indexBuffer를 묶어서 Mesh라는 리소스로 묶어서 관리를 했었다.

 

2. Mesh 클래스 생성하기

탐색기에서 Mesh 클래스를 강의자료에서 Engine 프로젝트 폴더에 복붙 한 다음에

Engine/00.Engine/Resource 필터에 넣어준다.

#pragma once
#include "ResourceBase.h"
#include "Geometry.h"

class Mesh : public ResourceBase
{
    using Super = ResourceBase;

public:
    Mesh();
    virtual ~Mesh();

	void CreateQuad();
	void CreateCube();
	void CreateGrid(int32 sizeX, int32 sizeZ);
	void CreateSphere();

	shared_ptr<VertexBuffer> GetVertexBuffer() { return _vertexBuffer; }
	shared_ptr<IndexBuffer> GetIndexBuffer() { return _indexBuffer; }

private:
	void CreateBuffers();

private:
	// Mesh
	shared_ptr<Geometry<VertexTextureNormalData>> _geometry;
	shared_ptr<VertexBuffer> _vertexBuffer;
	shared_ptr<IndexBuffer> _indexBuffer;
};
#include "pch.h"
#include "Mesh.h"
#include "GeometryHelper.h"

Mesh::Mesh()  : Super(ResourceType::Mesh)
{

}

Mesh::~Mesh()
{

}

void Mesh::CreateQuad()
{
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	GeometryHelper::CreateQuad(_geometry);
	CreateBuffers();
}

void Mesh::CreateCube()
{
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	GeometryHelper::CreateCube(_geometry);
	CreateBuffers();
}

void Mesh::CreateGrid(int32 sizeX, int32 sizeZ)
{
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	GeometryHelper::CreateGrid(_geometry, sizeX, sizeZ);
	CreateBuffers();
}

void Mesh::CreateSphere()
{
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	GeometryHelper::CreateSphere(_geometry);
	CreateBuffers();
}

void Mesh::CreateBuffers()
{
	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_geometry->GetVertices());
	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_geometry->GetIndices());
}

_geometry, _vertexBuffer, _indexBuffer 3총사를 들고 있다는 게 가장 핵심이라고 볼 수 있다.

먼저 원하는 도형을 만들어 주고, CreateBuffers에서 _vertexBuffer, _indexBuffer를 채워주는 원리다.

 

3. ResourceManager에서 DefaultMesh 만들어 주기

한번 만들어 줬으면 큐브를 100개 만들어 줬다고 해도 100번씩 Mesh를 만들 필요가 없었다.

공통적으로 공용적으로 사용하는 걸 Resource라고 했다.

얘도 ResourceManager를 통해 만드는게 일반적이라 보는 거고,

CreateDefaultMesh 같은 경우,

ResourceManager.cpp에서 주석처리 했던

#include "Mesh.h"

를 되살리고,

void ResourceManager::Init()
{
	CreateDefaultMesh();
}

void ResourceManager::CreateDefaultMesh()
{
	{
		shared_ptr<Mesh> mesh = make_shared<Mesh>();
		mesh->CreateQuad();
		Add(L"Quad", mesh);
	}
	{
		shared_ptr<Mesh> mesh = make_shared<Mesh>();
		mesh->CreateCube();
		Add(L"Cube", mesh);
	}
	{
		shared_ptr<Mesh> mesh = make_shared<Mesh>();
		mesh->CreateSphere();
		Add(L"Sphere", mesh);
	}
}

이것도 되살려서

기본적으로 Quad, Cube, Sphere는 많이 사용할 것이니까, 기본적인 형태로 넣어서 사용하는 것을 고려할 수 있다.

 

ResourceManager를 초기화(Init)를 하게 되면 DefaultMesh가 만들어질 테니까 그제야 Quad, Cube, Sphere 등등을 꺼내서 쓰면 된다.

여기에서 Grid가 없는건 크기에 따라서 너무 다르기 때문에 공용 Derault Mesh라고 하기 애매해서 제거되어 있다.

 

4. MeshDemo에서 _geometry, _vertexBuffer, _indexBuffer, _world, _texture 변수를 삭제하고 _obj하나만 두기

Engine을 빌드하고

Mesh를 사용하기 위해서는

MeshDemo.h에 가서

// Object
	shared_ptr<Geometry<VertexTextureNormalData>> _geometry;
	shared_ptr<VertexBuffer> _vertexBuffer; 
	shared_ptr<IndexBuffer> _indexBuffer; 
	Matrix _world = Matrix::Identity;

	shared_ptr<Texture> _texture;

이 부분을 묶어서 깔끔하게 관리할 수 있게 된다.

// Object
	shared_ptr<GameObject> _obj;

 

 

5. MeshRenderer 컴포넌트

Mesh는 Texture 처럼 Resource에 불과하다.

이 Mesh를 이용해서 그려주는 역할을 하는 Component를 추가해야 한다..

유니티에서는 MeshRenderer라는 애가 있다.

 

1) MeshRenderer 클래스 생성하기

2D 할 때 만들어 봤는데 3D에선 생각할 부분이 있다.

MeshRenderer 클래스를 Engine 프로젝트에 복붙하고

Engine/04. Component 필터에 넣어준다.

#pragma once
#include "Component.h"

class Mesh;
class Shader;

class MeshRenderer : public Component
{
	using Super = Component;
public:
	MeshRenderer();
	virtual ~MeshRenderer();

	virtual void Update() override;

	void SetMesh(shared_ptr<Mesh> mesh) { _mesh = mesh; }
	void SetTexture(shared_ptr<Texture> texture) { _texture = texture; }
	void SetShader(shared_ptr<Shader> shader) { _shader = shader; }

private:
	shared_ptr<Mesh> _mesh;
	shared_ptr<Texture> _texture;
	shared_ptr<Shader> _shader;
};
#include "pch.h"
#include "MeshRenderer.h"
#include "Camera.h"
#include "Game.h"
#include "Mesh.h"
#include "Shader.h"

MeshRenderer::MeshRenderer() : Super(ComponentType::MeshRenderer)
{

}

MeshRenderer::~MeshRenderer()
{

}

void MeshRenderer::Update()
{
	if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
		return;

	auto world = GetTransform()->GetWorldMatrix();
	_shader->GetMatrix("World")->SetMatrix((float*)&world);
	
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
	
	// TEMP
	Vec3 lightDir = {0.f, 0.f, 1.f};
	_shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);

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

shared_ptr<Mesh> _mesh; 는 실제 모형을 나타내는 거고,

shared_ptr<Texture> _texture; 는 대부분 uv 매핑을 통해 그릴 것이기 때문에 _texture를 두고 있지만 사실 shader에 막바로 texture를 꽂아줘도 되고 이거는 사실 옵션이긴 하다.

shared_ptr<Shader> _shader; 는 Material을 포함해서 사용할 수 있는 존재이다.

 

2) MeshRenderer::Update에서_mesh, _texture, _shader를 세팅하고 그리게 하기

일단 texture가 있다고 가정하고 meshRenderer를 넣어 보자면

MeshRenderer::Update를 하면서 원래 우리가 밖에 넣어놨던 코드들

void MeshDemo::Render()
{
	// ? 
	_shader->GetMatrix("World")->SetMatrix((float*)&_world);
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get()); 
	_shader->GetVector("LightDir")->SetFloatVector((float*)&_lightDir);

  	uint32 stride = _vertexBuffer->GetStride(); 
	uint32 offset = _vertexBuffer->GetOffset(); 

	// DeviceContext에서 IA단계에서 사용할 VertextBuffer를 묶어주는 함수였다.
	DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	_shader->DrawIndexed(0,0, _indexBuffer->GetCount(), 0, 0); 
}

LIghtDir는 어디서 받아 올지 고민이지만 나머지 여기와 관련된 부분들 World, View, Projection, Texture0를 다 공용적으로 들고 있다고 하면 이런 부분들을 싹 다 긁어가지고 MeshRenderer 쪽에다가 넣어 줄 수 있을 것이다.

 

MeshRenderer의 Update 할 때 역할은

void MeshRenderer::Update()
{
	if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
		return;

	auto world = GetTransform()->GetWorldMatrix();
	_shader->GetMatrix("World")->SetMatrix((float*)&world);
	
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
	
	// TEMP
	Vec3 lightDir = {0.f, 0.f, 1.f};
	_shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);

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

현재 나의 WorldMatrix를 가져와서 World에 세팅해 주고,

View와 Projection은 언젠가 카메라에 의해 세팅이 되어 있는 부분이겠고,

Texture를 세팅하고,

그다음에 LightDIr는 어떻게 할지 고민이 되지만 나중에 LightManager를 두거나 어디서 전역으로 쓸 수 있게 만들어 줘야 한다.

나머지 부분들은 Rendering 하는 코드가 여기에 싹 들어간다고 볼 수 있는 것이고,

MeshRenderer라는 부품 자체가 Mesh를 Rendering 하는 부품이다. 말장난 같지만, Mesh를 화면에 그리기 위한 부품이라고 했으니까 이것만 들고 있게 되면 _mesh, _texture, _shader 여깄는 정보들을 세팅하면 그려지는 부분들도 얘가 같이 처리하는 게 합리적인 선택이 될 수 있다.

 

6. MeshDemo::Init에서 GetMeshRenderer를 이용하여 shader, mesh, texture를 세팅하기

Demo를 만들 때는 Demo::Render에 온갖 기능들을 다 넣고 있었지만

이게 조금만 규모가 커진다고 생각하면

몬스터나 플레이어를 몇 백 마리씩 관리해야 되는데 몇 백 마리를 Demo::Render 여기다가 넣어 줄 것인가

당연히 그렇지 않을 것이다.

부품화 해서 케어할 준비를 끝내야 한다.

 

지금 선택한 방법은 MeshRenderer를 Engine단에서 만들어서 표준 쉐이더에 의한 그 렌더링 방식을 제공해 주게 될 것이고, 그 제공된 걸 토대로 Client 단에서 사용하는 방식이 된다고 볼 수 있다.

Light는 어떻게 할지 나중에 다시 고민을 해보고 일단 포함이 되어 있다고 가정을 하면

#pragma once
#include "IExecute.h"
#include "Geometry.h"

class MeshDemo : public IExecute
{	
public: 
	void Init() override; 
	void Update() override; 	 
	void Render() override; 

	shared_ptr<GameObject> _obj; 
	shared_ptr<GameObject> _camera;
};

MeshDemo 쪽에서는 아주 깔끔하게 내가 들고 있어야 하는 건 두 오브젝트 밖에 없을 것이고,

심지어 얘네들도 나중에 가면 씬이라는 개념으로 빠져가지고 씬이 이 GameObject를 들고 있는 게 일반적일 것이다.

 

1) MeshDemo::Init에서 _geometry, _vertexBuffer, _indexBuffer를 Create 하는 부분 삭제하기

09. MeshDemo.cpp에서

MeshDemo::Init에서

// Object
	_geometry = make_shared<Geometry<VertexTextureNormalData>>();
	//GeometryHelper::CreateCube(_geometry); 
	GeometryHelper::CreateSphere(_geometry);
	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_geometry->GetVertices());
	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_geometry->GetIndices());

이 부분을 삭제하고,

코드를 수정한다.

2) GameObject에서 GetMeshRenderer함수 복원하기

GameObject.h에서 주석처리 했던

class MeshRenderer;
shared_ptr<MeshRenderer> GetMeshRenderer();

를 복원하고,

 

GameObject.cpp의

shared_ptr<MeshRenderer> GameObject::GetMeshRenderer()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::MeshRenderer);
	return static_pointer_cast<MeshRenderer>(component);
}

를 주석을 해제한다.

 

#include "MeshRenderer.h"

도 주석을 해제한다.

그리고 Engine을 빌드하면 빌드가 된다.

 

3) MeshDemo::Init에서 GetMeshRenderer를 이용하여 shader, mesh, texture 값을 세팅하기

MeshDemo::Init에서 

void MeshDemo::Init()
{
	// Camera
	_camera = make_shared<GameObject>(); 
	_camera->GetOrAddTransform(); 
	_camera->AddComponent(make_shared<Camera>()); 
	_camera->AddComponent(make_shared<CameraScript>()); 

	// Object
	_obj = make_shared<GameObject>(); 
	_obj->GetOrAddTransform(); 
	_obj->AddComponent(make_shared<MeshRenderer>());
	{
		auto shader = make_shared<Shader>(L"07. Normal.fx"); 
		_obj->GetMeshRenderer()->SetShader(shader);
	}
	{
		RESOURCES->Init(); 
		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); 
	}
}

GetMeshRenderer를 이용하여 shader, mesh, texture를 세팅한다.

 

7. MeshDemo::Render의 코드를 지우고, MeshDemo::Update에서 _obj->Update를 호출해 컴포넌트인 MeshRenderer->Update()를 호출하기

void MeshDemo::Render()
{
	// ? 
	_shader->GetMatrix("World")->SetMatrix((float*)&_world);
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get()); 
	_shader->GetVector("LightDir")->SetFloatVector((float*)&_lightDir); 

	uint32 stride = _vertexBuffer->GetStride(); 
	uint32 offset = _vertexBuffer->GetOffset(); 

	// DeviceContext에서 IA단계에서 사용할 VertextBuffer를 묶어주는 함수였다.
	DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
	DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	// 원래 그렸던 device를 이용한 Draw에서는 technique, pass라는 인자가 없었어. 
	// 이거 만들어준 Shader라는 클래스에 포함이 되어 있는 기능이다. 
	// technique이란 pass를 골라줄 수 있다는 특징이 있다. 
	// _buffer->GetCount()이걸로 넣어줘도 되지만 몇 개 없으니 3으로 넣어준다.
	//_shader->Draw(1, 0, 3); 
	_shader->DrawIndexed(0,0, _indexBuffer->GetCount(), 0, 0); 
}

void MeshDemo::Render() 안의 코드를 삭제하고,

 

 

void MeshDemo::Update()
{
	_camera->Update(); 
	_obj->Update(); 
}

이렇게 해주면

_obj가 들고 있는 모든 Component들을 대상으로 Update가 호출이 될 것이고,

 

Component 중 MeshRenderer가 하는 역할 자체가 물체를 그리는 역할을 맡아주고 있었기 때문에

void MeshRenderer::Update()
{
	if (_mesh == nullptr || _texture == nullptr || _shader == nullptr)
		return;

	auto world = GetTransform()->GetWorldMatrix();
	_shader->GetMatrix("World")->SetMatrix((float*)&world);
	
	_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
	_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
	_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
	
	// TEMP
	Vec3 lightDir = {0.f, 0.f, 1.f};
	_shader->GetVector("LightDir")->SetFloatVector((float*)&lightDir);

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

Update 되면서 그려주는 부분을 처리해 주게 될 것이다.

 

8. 실행하기

Client를 빌드를 해보자.

에러가 난다.

EnginePch.h에서

#include "Mesh.h"

의 주석을 해제한다.

Engine을 Rebuild 하고 Client를 빌드하면 빌드가 된다.

 

모든 것들을 MeshDemo::Init에서 조립하고,

마지막에 MeshDemo::Update에서 _obj→Update()를 하면

MeshRenderer가 모든 것들을 신경 써 줄 것이기 때문에

복잡한 렌더링을 이리저리 뛰어다니며 할 필요 없이 해도 정상적으로 동작을 한다는 것을 알 수 있다.

 

조삼모사 같고 MeshDemo::Init에서 조립하는 게 오히려 귀찮은 게 아닌가 싶겠지만

이게 코드로 할 때는 귀찮아 보일 수 있지만 나중에 유니티, 언리얼에서 처럼 툴로 드래그 앤 드롭하는 건 언제든지 만들 수 있고, 이거를 조립한 형태를 prefab이라는 하나의 리소스로 관리할 수도 있다.

한 번만 만들면 재사용이 되는 거라서 편리하게 관리할 수 있을 것이다.

 

mesh라는 걸 조립을 해서 사용할 것이기 때문에 언제라도 완성된 결과물이 필요하면 이런 식으로 부품을 조립해서 사용하면 된다가 오늘의 핵심이었다.

 

9. 맺음말

지난 시간 내용을 복습을 할 때 08. NormalDemo 부분이 가장 중요하다고 볼 수 있다. 이해가 안 가면 반복해서 보자.

 

Material의 개념이 Shader에 통합이 되면서 편리하게 이것저것 설정할 수 있기 때문에 기존에 테스트하고 싶었던 여러 가지 들을 연습을 해보면서 작업을 해보길 바란다.

 

셰이더 같은 경우에도 50번대까지 할 수 있을 텐데 그 과정이 지루하지 않을 것이다.

지금은 별 게 없다. 빛도 다양해질 것이다.

 

파이프라인이 정리가 되었기 때문에 이제 구현부만 신경 써서 만들 수 있게 되었다.

 

다음 시간에는 imgui 툴을 사용하, 메쉬를 fbx포맷에 실제 메쉬를 로드하고, 거기다가 애니메이션을 붙이는 것을 만들어 보면 돌아다니는 플레이어까지 만들 수 있고, 땅을 깔아서 돌아다니는 플레이어에다가 조명 씌워서 만들 수 있으면 그때부터는 유니티나 언리얼이나 별 다른 차이가 없어진다.

 

콘텐츠 제작 능력에 따라 작업이 일어나는 것이고, 거기다가 서버를 붙이면 더 완벽하다.

근본적으로 봤을 때는 여기서 크게 달라지는 부분이 없을 것이다.

 

아직 이해가 가지 않은 부분이 있으면 복습하고 정리한다.

 

타이핑을 하건 복붙을 하건 확실히 이해하고 있는지 다시 만들어 보면 좋을 것이다.

반응형

'DirectX' 카테고리의 다른 글

43. Light, Material_Depth Stencil View  (0) 2024.02.11
42. Light, Material_Global Shader  (0) 2024.02.11
40. DirectX11 3D 입문_Normal  (0) 2024.02.09
39. DirectX11 3D 입문_HeightMap  (0) 2024.02.08
38. DirectX11 3D 입문_Sampling  (0) 2024.02.07

댓글