[목차]

 

- 클래스 정의

- Ext_DirectXMesh

- Ext_DirectXMaterial

- Set Unit

 

 

[클래스 정의]

 

렌더링 파이프라인 설정값을 렌더링될 Object마다 원하는 세팅으로 설정해주면 좋을 것입니다. 이렇게 하는게 가능한 이유가 해당 프레임워크에서는 리소스를 만들때마다 리소스 매니저를 통해 값들을 이름으로 저장하고 있기 때문입니다.

 

이걸 십분 활용해서 Ext_DirectXMesh 클래스와 Ext_DirectXMaterial 클래스를 만들었습니다. Mesh 클래스는 InputAssembler 단계에 대한 정보를 담은 클래스, Material 클래스는 그 외의 모든 단계에 대한 정보를 담은 클래스입니다.

 

사용 방식은 다음과 같습니다.

#include "PrecompileHeader.h"
#include "CubeActor.h"

#include <DirectX11_Extension/Ext_MeshComponent.h>
#include <DirectX11_Extension/Ext_Transform.h>

void CubeActor::Start()
{
	GetTransform()->SetWorldPosition({ 0.f, 0.f, 200.0f });
	GetTransform()->SetWorldScale({ 100.f, 100.f, 100.f }); // 크기 확대
	std::shared_ptr<Ext_MeshComponent> MeshComp = CreateComponent<Ext_MeshComponent>("BasicMesh", true);
	MeshComp->CreateMeshComponentUnit("Box", "Basic");
}

 

현재의 프레임워크 구조에서, Actor 내부에서 Component를 Create 하는 함수를 호출하면 Component가 생성됩니다. 여기서 MeshComponent의 경우 CreateMeshComponentUnit() 함수를 호출하면서 Mesh와 Material을 이름으로 하여 세팅하도록 설계해뒀습니다. 이러면 이름으로 값을 찾아서 내부에서 세팅해줍니다. 물론 먼저 만들어둔 값이 있어야 할 것입니다.

 

 

[Ext_DirectXMesh]

 

먼저 Mesh 클래스입니다.

////////////////////////// Ext_DirectXMesh.h
#pragma once
#include "Ext_ResourceManager.h"
#include "Ext_DirectXVertexBuffer.h"
#include "Ext_DirectXIndexBuffer.h"

// 만들어진 버텍스들의 정보를 저장하기 위한 클래스
class Ext_DirectXMesh : public Ext_ResourceManager<Ext_DirectXMesh>
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXMesh() {}
	~Ext_DirectXMesh() {}

	// delete Function
	Ext_DirectXMesh(const Ext_DirectXMesh& _Other) = delete;
	Ext_DirectXMesh(Ext_DirectXMesh&& _Other) noexcept = delete;
	Ext_DirectXMesh& operator=(const Ext_DirectXMesh& _Other) = delete;
	Ext_DirectXMesh& operator=(Ext_DirectXMesh&& _Other) noexcept = delete;

	// 버텍스 버퍼와 인덱스 버퍼 정보 입력 및 메시 생성
	static std::shared_ptr<Ext_DirectXMesh> CreateMesh(std::string_view _Name)
	{
		return CreateMesh(_Name, _Name, _Name);
	}

	// 버텍스 버퍼와 인덱스 버퍼 정보 입력 및 생성
	static std::shared_ptr<Ext_DirectXMesh> CreateMesh(std::string_view _Name, std::string_view _VBName, std::string_view _IBName)
	{
		std::shared_ptr<Ext_DirectXMesh> NewMesh = Ext_ResourceManager::CreateNameResource(_Name);
		NewMesh->VertexBufferPtr = Ext_DirectXVertexBuffer::Find(_VBName);
		NewMesh->IndexBufferPtr = Ext_DirectXIndexBuffer::Find(_IBName);

		if ((nullptr == NewMesh->VertexBufferPtr) || (nullptr == NewMesh->IndexBufferPtr))
		{
			MsgAssert("메시 생성 실패");
		}

		return NewMesh;
	}

	// Getter
	std::shared_ptr<class Ext_DirectXVertexBuffer> GetVertexBuffer() { return VertexBufferPtr; }
	std::shared_ptr<class Ext_DirectXIndexBuffer> GetIndexBuffer() { return IndexBufferPtr; }

protected:
	
private:
	D3D11_PRIMITIVE_TOPOLOGY Topology = D3D11_PRIMITIVE_TOPOLOGY::D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
	std::shared_ptr<class Ext_DirectXVertexBuffer> VertexBufferPtr; // 상수버퍼 데이터 저장용
	std::shared_ptr<class Ext_DirectXIndexBuffer> IndexBufferPtr; // 인덱스버퍼 데이터 저장용
	
	void MeshSetting(); // InputAssembler1(), InputAssembler2() 호출
	void InputAssembler1(); // 인풋어셈블러 1단계 실시, IASetVertexBuffers(), IASetPrimitiveTopology() 실시
	void InputAssembler2(); // 인풋어셈블러 2단계 실시, IndexBufferSetting() 호출
};

////////////////////////// Ext_DirectXMesh.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXMesh.h"
#include "Ext_DirectXVertexBuffer.h"
#include "Ext_DirectXIndexBuffer.h"
#include "Ext_DirectXDevice.h"

void Ext_DirectXMesh::MeshSetting()
{
	InputAssembler1();
	InputAssembler2();
}

void Ext_DirectXMesh::InputAssembler1()
{
	if (nullptr == VertexBufferPtr)
	{
		MsgAssert("버텍스 버퍼가 존재하지 않아서 인풋어셈블러1 과정을 실행할 수 없습니다.");
		return;
	}

	VertexBufferPtr->VertexBufferSetting();

	Ext_DirectXDevice::GetContext()->IASetPrimitiveTopology(Topology);
}

void Ext_DirectXMesh::InputAssembler2()
{
	// 그리는 순서에 대한 데이터를 넣어준다 // 012023
	if (nullptr == IndexBufferPtr)
	{
		MsgAssert("인덱스 버퍼가 존재하지 않아서 인풋 어셈블러2 과정을 실행할 수 없습니다.");
		return;
	}

	IndexBufferPtr->IndexBufferSetting();
}

 

Mesh 클래스는 Ext_DirectXVertexBuffer, Ext_DirectXIndexBuffer 정보를 가지고 있는 클래스입니다. 아래와 같이 사용해봅시다.

// 삼각형
{
	std::vector<Ext_DirectXVertexData> Vertices;
	Vertices.resize(3);

	Vertices[0] = { { 0.0f, 0.5f, 0.0 }, { 1, 0, 0, 1 } };
	Vertices[1] = { { 0.5f, -0.5f, 0.0f }, { 0, 1, 0, 1 } };
	Vertices[2] = { { -0.5f, -0.5f, 0.0f }, { 0, 0, 1, 1 } };

	std::vector<UINT> ArrIndex = { 0, 2, 1 };

	Ext_DirectXVertexBuffer::CreateVertexBuffer("Triangle", Vertices);
	Ext_DirectXIndexBuffer::CreateIndexBuffer("Triangle", ArrIndex);
	Ext_DirectXMesh::CreateMesh("Triangle");
}

// Rect
{
	std::vector<Ext_DirectXVertexData> ArrVertex;
	ArrVertex.resize(4);

	ArrVertex[0] = { { -0.5f,  0.5f, 0.0f, 1.0f }, { 1, 0, 0, 1 }, /*{ 0.0f, 0.0f }*/ };
	ArrVertex[1] = { {  0.5f,  0.5f, 0.0f, 1.0f }, { 0, 1, 0, 1 }, /*{ 1.0f, 0.0f }*/ };
	ArrVertex[2] = { { -0.5f, -0.5f, 0.0f, 1.0f }, { 0, 0, 1, 1 }, /*{ 1.0f, 1.0f }*/ };
	ArrVertex[3] = { {  0.5f, -0.5f, 0.0f, 1.0f }, { 1, 1, 0, 1 }, /*{ 0.0f, 1.0f }*/ };

	std::vector<UINT> ArrIndex = { 0, 2, 1, 2, 3, 1 };

	Ext_DirectXVertexBuffer::CreateVertexBuffer("Rect", ArrVertex);
	Ext_DirectXIndexBuffer::CreateIndexBuffer("Rect", ArrIndex);
	Ext_DirectXMesh::CreateMesh("Rect");
}

 

CreateVertexBuffer(), CreateIndexBuffer()을 실시할 때 Triangle, Rect라는 이름으로 저장해줬습니다. 이러면 CreateMesh()를 실시할 때 동일한 이름을 넣어줘서 내부에서 Triangle, Rect라는 이름의 Vertex Buffer, Index Buffer를 Find()한 뒤 저장해두는 것입니다. 이러면 Mesh 클래스 하나로 두 가지 클래스를 바로 활용할 수 있게 됩니다.

 

 

[Ext_DirectXMaterial]

 

다음은 Material 클래스입니다.

////////////////////// Ext_DirectXMaterial.h
#pragma once
#include "Ext_ResourceManager.h"

// 렌더링 파이프라인 세팅 실시를 위한 클래스
class Ext_DirectXMaterial : public Ext_ResourceManager<Ext_DirectXMaterial>
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXMaterial() {};
	~Ext_DirectXMaterial() {};

	// delete Function
	Ext_DirectXMaterial(const Ext_DirectXMaterial& _Other) = delete;
	Ext_DirectXMaterial(Ext_DirectXMaterial&& _Other) noexcept = delete;
	Ext_DirectXMaterial& operator=(const Ext_DirectXMaterial& _Other) = delete;
	Ext_DirectXMaterial& operator=(Ext_DirectXMaterial&& _Other) noexcept = delete;

	// 머티리얼 생성
	static std::shared_ptr<class Ext_DirectXMaterial> CreateMaterial(const std::string_view& _Name)
	{
		std::shared_ptr<class Ext_DirectXMaterial> NewRes = Ext_ResourceManager<Ext_DirectXMaterial>::CreateNameResource(_Name);
		return NewRes;
	}

	// 머티리얼 세팅 실시
	void SetVertexShader(std::string_view _Name);
	void SetPixelShader(std::string_view _Name);
	void SetBlendState(std::string_view _Name);
	void SetDepthState(std::string_view _Name);
	void SetRasterizer(std::string_view _Name);

	// Getter, Setter
	std::shared_ptr<class Ext_DirectXVertexShader> GetVertexShader() { return VertexShader; }
	std::shared_ptr<class Ext_DirectXPixelShader> GetPixelShader() { return PixelShader; }

protected:
	
private:
	// 렌더링 파이프라인 세팅
	void MaterialSetting(); // 아래의 함수들 차례대로 실행
	void VertexShaderSetting();
	void HullShaderSetting();
	void TessellatorSetting();
	void DomainShaderSetting();
	void GeometryShaderSetting();
	void RasterizerSetting();
	void PixelShaderSetting();
	void OutputMergerSetting();

	std::shared_ptr<class Ext_DirectXVertexShader> VertexShader; // 버텍스 셰이더 저장
	std::shared_ptr<class Ext_DirectXPixelShader> PixelShader; // 픽셀 셰이더 저장
	
};

////////////////////// Ext_DirectXMaterial.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXMaterial.h"
#include <DirectX11_Base/Base_String.h>

#include "Ext_DirectXVertexShader.h"
#include "Ext_DirectXPixelShader.h"

void Ext_DirectXMaterial::SetVertexShader(std::string_view _Name)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	VertexShader = Ext_DirectXVertexShader::Find(UpperName);

	if (nullptr == VertexShader)
	{
		MsgAssert("존재하지 않는 버텍스셰이더를 세팅할 순 없음");
		return;
	}
}

void Ext_DirectXMaterial::SetPixelShader(std::string_view _Name)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	PixelShader = Ext_DirectXPixelShader::Find(UpperName);

	if (nullptr == PixelShader)
	{
		MsgAssert("존재하지 않는 픽셀 셰이더를 세팅할 순 없음");
	}
}

void Ext_DirectXMaterial::SetBlendState(std::string_view _Name)
{
}

void Ext_DirectXMaterial::SetDepthState(std::string_view _Name)
{
}

void Ext_DirectXMaterial::SetRasterizer(std::string_view _Name)
{
}

void Ext_DirectXMaterial::MaterialSetting()
{
	VertexShaderSetting();
	// HullShader();
	// Tessellator();
	// DomainShader();
	// GeometryShader();
	// Rasterizer();
	PixelShaderSetting();
	OutputMergerSetting();
}

void Ext_DirectXMaterial::VertexShaderSetting()
{
	if (nullptr == VertexShader)
	{
		MsgAssert("버텍스 쉐이더가 존재하지 않아서 버텍스 쉐이더 과정을 실행할 수 없습니다.");
		return;
	}

	VertexShader->VertexShaderSetting();
}

void Ext_DirectXMaterial::HullShaderSetting()
{

}
void Ext_DirectXMaterial::TessellatorSetting()
{

}
void Ext_DirectXMaterial::DomainShaderSetting()
{

}
void Ext_DirectXMaterial::GeometryShaderSetting()
{
}

void Ext_DirectXMaterial::RasterizerSetting()
{
}

void Ext_DirectXMaterial::PixelShaderSetting()
{
	if (nullptr == PixelShader)
	{
		MsgAssert("픽셀 쉐이더가 존재하지 않아서 픽셀 쉐이더 과정을 실행할 수 없습니다.");
		return;
	}

	PixelShader->PixelShaderSetting();
}

void Ext_DirectXMaterial::OutputMergerSetting()
{
}

 

머티리얼 클래스는 InputAssembler 이외의 단계를 진행하기 위한 정보들을 담고, 렌더링 파이프라인 세팅 순간에 그 정보들을 가져와 Setting을 실시해주는 클래스입니다.

 

물론 각 정보들은 이미 만들어져 있어야하며, 지금은 Vertex Shader와 Pixel Shader만 만들어졌기 때문에 이 값들만 저장하고 있는 클래스가 될 것입니다. 나중에 단계들을 배우고 추가하게 되면 여기에 바로바로 추가해주면 됩니다. 우선은 아래와 같이 사용하면 됩니다.

// 일반(단일 메시)
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("Basic");
	NewRenderingPipeline->SetVertexShader("Basic_VS");
	NewRenderingPipeline->SetPixelShader("Basic_PS");
	NewRenderingPipeline->SetBlendState("BaseBlend"); // 아직 없음
	NewRenderingPipeline->SetDepthState("EngineDepth"); // 아직 없음
	NewRenderingPipeline->SetRasterizer("EngineRasterizer"); // 아직 없읍
}

// 다양한 텍스쳐가 있는 메시
{
	std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("PBR");
	NewRenderingPipeline->SetVertexShader("Basic_VS");
	NewRenderingPipeline->SetPixelShader("PBR_PS");
	NewRenderingPipeline->SetBlendState("BaseBlend"); // 아직 없음
	NewRenderingPipeline->SetDepthState("EngineDepth"); // 아직 없음
	NewRenderingPipeline->SetRasterizer("EngineRasterizer"); // 아직 없음
}

 

이러면 Basic, PBR이라는 이름으로 Material들이 값들을 하나로 묶어서 저장하기 때문에, 클래스 하나만 써서 바로 활용할 수 있게 됩니다.

 

 

[Set Unit]

 

이제 다시 MeshComponentUnitInitialize() 함수로 돌아가보겠습니다. 함수 내에서는 앞서 생성해둔 Mesh와 Material 정보를 가져와 자신에게 세팅해줍니다. 그리고 InputLayout을 Create해서 Mesh, Material, InputLayout 값을 지니고 있게 됩니다.

// 메시 컴포넌트 유닛 생성 시 호출, Mesh, Material, ConstantBuffer 세팅
void Ext_MeshComponentUnit::MeshComponentUnitInitialize(std::string_view _MeshName, std::string_view _MaterialName)
{
	Mesh = Ext_DirectXMesh::Find(_MeshName); // 메시 설정
	Material = Ext_DirectXMaterial::Find(_MaterialName); // 머티리얼 설정
	
	if (nullptr == Mesh || nullptr == Material)
	{
		MsgAssert("존재하지 않는 메시나 머티리얼을 메시유닛에 넣을 수는 없습니다.")
	}

	InputLayout->CreateInputLayout(Mesh->GetVertexBuffer(), Material->GetVertexShader()); // InputLayout 설정

	// 상수버퍼 세팅
	// [1] 버텍스 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& VertexShaderBuffers = Material->GetVertexShader()->GetBufferSetter();
	BufferSetter.Copy(VertexShaderBuffers);

	// [2] 픽셀 셰이더 정보 가져오기
	const Ext_DirectXBufferSetter& PixelShaderBuffers = Material->GetPixelShader()->GetBufferSetter();
	BufferSetter.Copy(PixelShaderBuffers);

	// [3] 트랜스폼 상수버퍼 세팅하기
	const TransformData& Data = *(OwnerMeshComponent.lock()->GetTransform()->GetTransformData().get());
	BufferSetter.SetConstantBufferLink("TransformData", Data);

	// [4] 카메라에 넣기
	GetOwnerMeshComponent().lock()->GetOwnerCamera().lock()->PushMeshComponentUnit(GetSharedFromThis<Ext_MeshComponentUnit>(), RenderPath::Unknown);
}

 

이러면 MeshComponent의 계층 구조는 다음과 같아질 것입니다.

MeshComponent
└── MeshComponentUnit
    ├── Ext_DirectXMesh
    │   ├── Ext_DirectXVertexBuffer
    │   └── Ext_DirectXIndexBuffer
    ├── Ext_DirectXMaterial
    │   ├── Ext_DirectXVertexShader
    │   ├── Ext_DirectXPixelShader
    │   ├── Ext_DirectXBlend
    │   ├── Ext_DirectXDepth
    │   └── Ext_DirectXRasterizer
    └── Ext_DirectXInputLayout

 

InputLayout을 여기서 Create() 하는 이유는 InputLayout이 Vertex Shader 컴파일 후 호출되어야 하기 때문입니다.

void Ext_DirectXInputLayout::CreateInputLayout(std::shared_ptr<Ext_DirectXVertexBuffer> _VertexBuffer, std::shared_ptr< Ext_DirectXVertexShader> _VertexShader)
{
	Relase(); // 한번 해줘야 CreateInputLayout가 정상 동작함

	if (nullptr == _VertexBuffer->GetInputLayout())
	{
		MsgAssert("레이아웃 정보를 만들수 없는 버텍스 버퍼 입니다.");
	}

	const std::vector<D3D11_INPUT_ELEMENT_DESC>& LayOutInfo = _VertexBuffer->GetInputLayout()->GetInputLayoutDescs();

	// CreateInputLayout은 정점 버퍼 구조와 셰이더 입력 구조 간의 매핑을 정의
	HRESULT hr = Ext_DirectXDevice::GetDevice()->CreateInputLayout
	(
		&LayOutInfo[0],
		static_cast<UINT>(LayOutInfo.size()),
		_VertexShader->GetBinaryCode()->GetBufferPointer(),
		_VertexShader->GetBinaryCode()->GetBufferSize(),
		&InputLayout
	);

	if (S_OK != hr)
	{
		char Buffer[256] = {};
		sprintf_s(Buffer, "CreateInputLayout failed: 0x%08X", hr);
		MsgAssert(Buffer)
		return;
	}
}

 

const D3D11_INPUT_ELEMENT_DESC* 정점 입력 요소 배열의 시작 주소, 각 요소가 어떤 데이터인지 정의
UINT 위 배열의 요소 수(입력 요소가 몇개인가)
const void* 정점 셰이더의 바이트 코드 시작 주소, 여기에는 입력 시그니처가 포함되어 있어야 함
SIZE_T 위 바이트코드의 길이
ID3D11InputLayout** 호출의 결과로 생성될 InputLayout 객체의 포인터 주소 저장->IASetInputLayout에서 활용

 

위의 과정이 완료되면 셰이더 컴파일 진행 후 생성된 리소스 정보들(바인딩 슬롯은 몇번째인지, 슬롯 이름은 무엇인지)을 가져와서 저장해줍니다.

// 버퍼 세팅 복사/붙여넣기 실시
void Ext_DirectXBufferSetter::Copy(const Ext_DirectXBufferSetter& _OtherBufferSetter)
{
	for (const std::pair<std::string, ConstantBufferSetter>& Setter : _OtherBufferSetter.ConstantBufferSetters)
	{
		ConstantBufferSetters.insert(Setter);
	}
    //...
}

 

이러면 Unit에 있는 Setter에 값이 복사/붙여넣기 될 것입니다.

 

상수버퍼에 한해서는 매 프레임마다 실시간으로 렌더링 과정에 적용될 수 있도록 플래그를 세워뒀습니다. 근데 당연히 이 값은 어딘가에 존재해야하기 떄문에(실체가 있어야지 활용할 것), 할당된 상태여야 합니다.

 

하지만 리플렉션 과정에서는 값이 할당된 상태가 이니기 떄문에 주소 위치와 크기를 특정할 수 없어서 실제로 이 값을 활용하는 Unit에서 SetConstantBufferLink()를 실시해줍니다.

// 호출
const TransformData& Data = *(OwnerMeshComponent.lock()->GetTransform()->GetTransformData().get());
BufferSetter.SetConstantBufferLink("TransformData", Data);

// 상수 버퍼에 한하여 호출, cbuffer 슬롯 이름과 크기를 나중에 따로 지정해주기 위해 호출하는 함수
void Ext_DirectXBufferSetter::SetConstantBufferLink(std::string_view _Name, const void* _Data, UINT _Size)
{
	std::string UpperName = Base_String::ToUpper(_Name);
	std::multimap<std::string, ConstantBufferSetter>::iterator FindIter = ConstantBufferSetters.find(UpperName);

	if (ConstantBufferSetters.end() == FindIter)
	{
		MsgAssert("존재하지 않는 상수버퍼를 세팅하려고 했습니다." + UpperName);
		return;
	}

	std::multimap<std::string, ConstantBufferSetter>::iterator NameStartIter = ConstantBufferSetters.lower_bound(UpperName);
	std::multimap<std::string, ConstantBufferSetter>::iterator NameEndIter = ConstantBufferSetters.upper_bound(UpperName);

	for (; NameStartIter != NameEndIter; ++NameStartIter)
	{
		ConstantBufferSetter& BufferSetter = NameStartIter->second;

		if (BufferSetter.ConstantBuffer->GetBufferSize() != _Size)
		{
			MsgAssert("상수버퍼와 세팅하려는 데이터의 크기가 다릅니다. 상수버퍼 : " + std::to_string(BufferSetter.ConstantBuffer->GetBufferSize()) + "유저가 세팅한 데이터" + std::to_string(_Size) + UpperName);
			return;
		}

		BufferSetter.CPUData = _Data;
		BufferSetter.CPUDataSize = _Size;
	}
}

 

여기까지 완료했다면 정상적으로 Unit 마다의 렌더링 파이프라인 단계를 진행할 준비가 마무리 된 것입니다.

[목차]

 

- 셰이더에 대해 알아보려는 이유

- 셰이더란

- 상수 버퍼

- Vertex Shader 선언

- Pixel Shader 선언

- 셰이더 오토 컴파일-1

- 셰이더 오토 컴파일-2

 

 

[셰이더에 대해 알아보려는 이유]

 

https://umtimos.tistory.com/178

 

[DirectX11] InputAssembler 단계 준비와 도형 출력

[목차] - InputLayout(버텍스 정보) 정의- Vertex Buffer- Index Buffer- 버퍼 생성하기- 셰이더 컴파일- CreateInputLayout- 결과 출력- 예상치 못한 결과들 [InputLayout(버텍스 정보) 정의] 렌더링의 가장 기본 자원은

umtimos.tistory.com

 

해당 포스팅에서 삼각형, 사각형을 화면에 출력하기 위해 간단한 Vertex Shader, Pixel Shader를 작성하여 바로 컴파일 한 뒤 사용했습니다. 하지만 이후 진행될 단계들에서는 다양한 그래픽스 효과를 알아볼 것이고, 여기에는 조금 더 복잡한 기능을 갖거나 한 번에 많은 양의 셰이더를 사용하는 로직이 필요할 수 있습니다.

 

결국 이러면 셰이더의 양은 많아질 것이기 때문에, 이들을 저 포스팅을 진행했을 때와 같이 각각 컴파일하는 과정을 진행한다면 코드의 양이 늘어나고 복잡해질 것으로 예상되었습니다. 이에 따라 셰이더 컴파일의 구조를 변경할 겸, 셰이더와 상수버퍼를 알아본 뒤 넘어가고자 합니다.

 

 

[셰이더란]

 

셰이더(Shader)는 GPU에서 실행되는 작은 프로그램으로, DirectX에서는 HLSL(Hight Level Shader Language)을 사용하여 작성됩니다.

Vertex Shader 정점의 위치 변환
Pixel Shader 픽셀 단위 색상 계산
Geometry Shader 정점 간 도형 생성
Compute Shader 일반 계산용(병렬 연산)
Tessllation Shader 정점 세분화

 

다양한 종류가 있지만, 해당 프레임워크에서는 일단 Vertex Shader, Pixel Shader만 다루고 이후 시간적 여유가 생긴다면 Particle System을 만드는 과정을 진행함에 따라 다른 셰이더도 알아보도록 하겠습니다.

 

 

[상수 버퍼(Constant Buffer)]

 

셰이더를 알아보기 전에 먼저 Constant Buffer에 대해 알아보도록 하겠습니다. Constant Buffer도 Vertex Buffer, Index Buffer와 마찬가지로 버퍼입니다. 결국 셰이더(GPU)에서 활용할 수 있도록 렌더랑 파이프라인에 바인딩을 진행해줄 수 있어야 합니다. 일단 만드는 과정을 알아보겠습니다.

// CreateBuffer()를 통해 상수 버퍼를 생성
void Ext_DirectXConstantBuffer::CreateConstantBuffer(const D3D11_SHADER_BUFFER_DESC& _BufferDesc)
{
	ConstantBufferInfo.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	ConstantBufferInfo.ByteWidth = _BufferDesc.Size;
	ConstantBufferInfo.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE;
	if (0 == ConstantBufferInfo.CPUAccessFlags)
	{
		ConstantBufferInfo.Usage = D3D11_USAGE_DEFAULT;
	}
	else 
	{
		ConstantBufferInfo.Usage = D3D11_USAGE_DYNAMIC;
	}

	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateBuffer(&ConstantBufferInfo, nullptr, ConstantBuffer.GetAddressOf()))
	{
		MsgAssert("상수 버퍼 생성에 실패했습니다.");
		return;
	}
}

 

위의 함수는 이후 셰이더 리플렉션 과정에서 호출될 함수인데, 어쨋든 Constant Buffer도 만들어지려면 CreateBuffer를 호출해줘야 합니다. 하지만 여기서 다른 버퍼와 중요한 차이점이 하나 있는데, 바로 CPUAccessFlags가 D3D11_CPU_ACCESS_WRITE라는 점입니다.

 

이 부분이 중요한 이유는 이게 있어야 cbuffer로 등록한 값들에 대해 CPU가 접근해서 수정할 것임을 명시할 수 있기 때문입니다. Vertex Buffer와 Index Buffer는 한 번 정해지면 변경되지 않는 고정된 값으로 볼 수 있습니다. 하지만 Constant Buffer는 만들어진 후 매프레임마다 계속해서 변경됩니다. CPU가 계속 수정하고 GPU가 이걸 넘겨받아서 사용하는 것입니다.

 

이 과정을 위해 Map(), memcpy(), Unmap()을 활용합니다.

// Map(), Unmap() 실시
void Ext_DirectXConstantBuffer::ChangeData(const void* _Data, UINT _Size)
{
	// 머티리얼들은 상수버퍼나 이런걸 통일해 놓은 것이다.
	if (nullptr == _Data)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "에 nullptr인 데이터를 세팅하려고 했습니다.");
		return;
	}

	if (ConstantBufferInfo.ByteWidth != _Size)
	{
		std::string CurName = Name;
		MsgAssert(CurName + "크기가 다른 데이터가 들어왔습니다.");
		return;
	}

	D3D11_MAPPED_SUBRESOURCE SettingResources = { 0, };

	// 그래픽카드야 너한테 보낼께있어 잠깐 멈춰봐 
	// D3D11_MAP_WRITE_DISCARD 최대한 빠르게 처리하는 
	Ext_DirectXDevice::GetContext()->Map(ConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &SettingResources);

	if (SettingResources.pData == nullptr)
	{
		std::string CurName = Name;
		MsgAssert(CurName + " 그래픽카드에게 메모리 접근을 허가받지 못했습니다.");
		return;
	}
	memcpy_s(SettingResources.pData, ConstantBufferInfo.ByteWidth, _Data, ConstantBufferInfo.ByteWidth);
	Ext_DirectXDevice::GetContext()->Unmap(ConstantBuffer, 0);
}

 

+) 위의 방식이 아니면 UpdateSubresource()를 활용해야하는데, 이건 매프레임별 갱신이 불가능한 방식이라 실시간 렌더링을 진행해야하는 해당 프레임워크에서 적합한 방식이 아니라 따로 설명하지는 않겠습니다.

 

GPU는 기본적으로 병렬처리가 기본이기 때문에 언제 무슨일이 벌어질지 알 수 없습니다. 그렇다면 어떻게 싱글 스레드 연산(기본적으로)을 실시하는 CPU와 연동이 가능할까에 대한 해답이 Map() 함수입니다. 해당 함수를 호출하면 GPU에게 해당 값에 대해 변경이 이루어질 것이니 잠시 기다려달라고 말하는 것과 다름 없습니다. 이 함수 호출과 함께 memcpy로 전달할 값을 덮어쓴 뒤 Unmap() 함수로 작업이 끝났음을 알려줌으로써 CPU와 GPU를 동기화 시키는 것입니다. 이 과정을 마친 다음에야 상수 버퍼를 렌더링 파이프라인에 바인딩 시키는 것입니다.

 

 

[Vertex Shader 선언]

 

Vertex Shader를 다음과 같이 작성했습니다.

cbuffer TransformData : register(b0)
{
    float4 LocalPosition;
    float4 LocalRotation;
    float4 LocalScale;
    float4 LocalQuaternion;
    float4x4 LocalMatrix;

    float4 WorldPosition;
    float4 WorldRotation;
    float4 WorldQuaternion;
    float4 WorldScale;
    float4x4 WorldMatrix;

    float4x4 ViewMatrix;
    float4x4 ProjectionMatrix;
    float4x4 WorldViewMatrix;
    float4x4 WorldViewProjectionMatrix;
}

struct VSInput
{
    float4 Position : POSITION;
    float4 Color : COLOR;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
};

struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
    float4 TexCoord : TEXCOORD;
    float4 Normal : NORMAL;
};

VSOutput Basic_VS(VSInput _Input)
{
    VSOutput Output;
    _Input.Position.w = 1.0f;
    Output.Position = mul(_Input.Position, WorldViewProjectionMatrix);
    Output.Color = _Input.Color;
    Output.TexCoord = _Input.TexCoord;
    Output.Normal = _Input.Normal;
   
    return Output;
}

 

확인해보면 구조체로 보이는 것이 3개, 함수처럼 보이는 것이 한 개 있습니다. 하나씩 알아보도록 하겠습니다.

 

1. cbuffer

: 이게 위에서 말씀드린 Constant Buffer입니다. 여기서는 TransformData를 넘겨받는 것으로 되어있는데, 해당 프레임워크에서 이 값의 위치는 각 Component(정확히는 렌더링 컴포넌트)의 Trasnform 내부입니다. 렌더링 파이프라인 과정 진행 전 TransformData를 Updata하는 과정(이전에 말씀드렸던 것과 같이 카메라의 뷰, 프로젝션 행렬을 연산하는 과정)을 거치기 때문에 값이 변경될 것이고, Map, Unmap으로 변경된 값을 적용시킨 뒤 바인딩되어 여기로 전달됩니다.

 

그리고 일반적인 struct와는 다르게 register(b0)이라는 것이 달려있는데, 이것은 바인딩 슬롯입니다. 셰이더 프로그램에는 각자의 바인딩 슬롯이 있습니다. 종류는 크게 네 가지입니다.

 

[1] b 슬롯

: 상수버퍼, 슬롯 갯수는 14개(b0 ~ b13), 크기 제한 4096byte

 

[2] t 슬롯

: 텍스쳐(정확히는 셰이더 리소스 뷰(SRV) 바인딩 슬롯이긴 함), 슬롯 갯수는 128개(t0 ~ t127), 크기 제한 없음

 

[3] s 슬롯

: 샘플러, 슬롯 갯수는 16개(s0 ~ s15), 데이터 형태가 아니라 메모리 크기 개념 없음

 

[4] u 슬롯

: Unordered Access View(UAV) 바인딩 슬롯인데, 이건 Compute Shader 전용, 슬롯 갯수는 64개(u0 ~ u63), 크기 제한 없음

 

각 상수 버퍼들은 모두 각자의 바인딩 슬롯을 가지고 있어야 합니다. 예를 들어, 위에서 TransformData를 b0 슬롯에 바인딩한 상태인데, 다른 상수버퍼인 LightData가 바인딩되어야 한다고 가정해보겠습니다. 이러면 register(b0)에는 이미 TransformData가 바인딩되어 있기 때문에 register(b1)에 바인딩해줘야 합니다. 이런 식으로 상수 버퍼가 추가될 때마다 각자 고유한 슬롯에 바인딩해줄 수 있도록 합니다.

 

그렇다면 TransformData(b0), LightData(b1)이 바인딩 된 상태에서 텍스쳐(t) 상수 버퍼가 새로 바인딩 되어야한다면 어떻게 해야할까요? 이럴 경우에는 그냥 t0에 바인딩하면 됩니다. b랑 t는 서로 다른 슬롯이기 때문에 뒤에 숫자는 상관이 없습니다. 물론 또 다른 텍스쳐를 추가로 바인딩할 경우에는 t1에 바인딩하면 됩니다. 샘플러도 마찬가지입니다.

 

cbuffer에 대해서는 중요한 부분이 하나 더 있습니다. 바로 외부에서 전달하는 셰이더 내부에서 사용할 때의 크기가 완전히 동일해야 한다는 것입니다. 예를 들어, 외부에서 40byte를 전달해줬다면 내부에서도 40byte를 그대로 받아서 사용해야합니다. 물론 GPU는 크기만 맞다면 상관없이 그냥 써버리기 때문에 순서도 중요합니다.

 

이 부분에서 종종 실수할 수 있는 점이 하나 있는데, cbuffer는 내부가 16byte 단위로 정렬된다는 점입니다. 예를 들어 아래와 같이 데이터가 들어온다고 가정해보겠습니다.

cbuffer TransformData : register(b0)
{
    float4x4 WorldMatrix;  // 64 bytes
    float3 LightDir;       // 12 bytes
    float  Padding;        // 4 bytes → align 16 bytes
};

 

이러면 총 크기는 64 + 12 + 4 = 80이 됩니다. 만약 외부에서 이를 인지하지 않고 그냥 보내버리면 분명 동일한 형태로 보냈는데 값이 안맞을 수도 있습니다. 이런 실수를 방지하기 위해 외부에서도 그냥 전달할 구조체에 대해서는 alignas(16)을 실시해서 정렬을 해줘야 합니다.

 

2. VSInput

: 이 값은 CPU에서 정의한 뒤 바인딩해준 Vertex Buffer의 시멘틱들입니다. 여기서는 상수 버퍼와 조금 다른점이, 바깥에서 전달한 데이터와 여기서 받는 데이터의 크기가 굳이 같지 않아도 된다는 점입니다. 예를 들어 외부에서 POSITION, COLOR, TEXCOORD, NORMAL을 전달했다고 가정해봅시다. 이러면 cbuffer는 모두 동일하게 선언해줘야겠지만 Vertex Buffer는 그냥 순서만 맞으면 됩니다. 위에서는 그냥 동일하게 모두 동일하게 선언했지만, 난 그냥 두개만 쓸건데? 하면 아래와 같이 순서만 맞춰주면 된다는 것입니다.

struct VSInput
{
    float4 Position : POSITION;
    float4 TexCoord : TEXCOORD;
};

 

이렇게 정의해주면 셰이더의 엔트리포인트에 입력되어 활용됩니다.

 

3. VSOutput

: 이 부분은 연산 결과에 대해 어떤 값을 내보낼지 정의하는 구조체입니다. 이렇게 내보낸 값은 Rasterizer에서 활용하게 됩니다. 여기서 한 가지 중요한 점은 SV 태그가 붙은 시멘틱이 하나 있어야한다는 점입니다. 여기서는 POSITION에 SV를 붙였는데, 이건 시스템 시멘틱이라는 의미로 이 정점이 최종적으로 화면의 어디에 그려질 것인가를 GPU에게 알려주는 특별한 위치 정보라는 의미를 담고 있습니다.

 

Vertex Shader의 SV_POSITION 출력은 Rasterizer가 픽셀 위치 계산에 활용하게 되고, Pixel Shader의 SV_POSITION에 전달되어 현재 픽셀이 그려질 정확한 화면 좌표가 어딘지를 알아낼 수 있게 됩니다. 이외의 나머지 값은 단순한 속성(attribute)으로, 사용자가 넣고싶으면 넣고, 빼고싶으면 뺄 수 있는 값들 입니다.

 

 

[Pixel Shader 선언]

 

다음은 Pixel Shader입니다.

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
};

float4 Basic_PS(PSInput _Input) : SV_TARGET
{
    return _Input.Color;
}

 

위에서 VSOutput으로 전달한 값 중 SV 태그가 붙은 시멘틱은 필수적으로 받고, 나머지 받고싶은 속성을 적어주면 됩니다. 물론 순서는 지켜줘야합니다(안지키면 오류남). 여기서는 단순하게 SV_POSITION과 COLOR를 받고 출력하고 있습니다.

 

픽셀 셰이더는 일단 여기까지만 진행하고, 나중에 SRV(t슬롯), MRT(멀티렌더타겟)을 활용할 때 자세히 알아보도록 하겠습니다.

 

 

[셰이더 오토 컴파일-1]

 

셰이더를 작성했으니, 사용하려면 당연히 컴파일을 진행해야합니다. 하지만 앞으로 다양한 셰이더들이 작성될 것이기 때문에 매 셰이더마다 따로 컴파일하고 값을 저장한 뒤 활용하기는 어려워 셰이더 오토 컴파일 기능을 추가했습니다.

 

일단 오토 컴파일을 위해 이전에 만들어준 Directory 기능을 활용해줍니다. 이걸 활용하면 경로를 찾아가서 존재하는 hlsl 파일들을 모두 확인하여 절대 경로를 만들어줄 것입니다.

 

또한 해당 프로젝트에서는 컴파일을 위해 한 가지 규칙이 필요한데, 바로 셰이더 파일 이름과 엔트리 포인트 이름이 동일해야한다는 것입니다. 예를 들어, Basic_VS라는 이름의 Vertex Shader를 만들었다고 가정해봅시다. 이러면 Vertex Shader 내부의 엔트리 포인트 이름도 Basic_VS로 해줘야 정상적으로 컴파일이 진행됩니다.

// 셰이더 정보 생성(Shader 오토컴파일, ConstantBuffer)
void Ext_DirectXResourceLoader::ShaderCompile()
{
	// 셰이더 생성 규칙 : [1], [2]를 모두 만족해야 정상 컴파일 진행
	// [1] 이름 + "_" + "Type" == Basic_PS
	// [2] 내부 main(EntryPoint) 이름도 동일하게 설정
	Base_Directory Dir;
	Dir.MakePath("../Shader");
	std::vector<std::string> Paths = Dir.GetAllFile({ "hlsl" });
	for (const std::string& ShaderPath : Paths)
	{
		std::string EntryPoint = Dir.FindEntryPoint(ShaderPath);
		Ext_DirectXShader::ShaderAutoCompile(ShaderPath, EntryPoint.c_str());
	}
}

 

위의 기능을 호출하면 아래의 연관 함수들이 호출됩니다.

/////////////////////////////////////// Ext_DirectXVertexShader
Ext_DirectXVertexShader::Ext_DirectXVertexShader()
{
	Type = ShaderType::Vertex;
}

void Ext_DirectXVertexShader::CreateVertexShader(std::string_view _Path, std::string_view _EntryPoint, UINT _VersionHigh, UINT _VersionLow)
{
	CreateVersion("vs", _VersionHigh, _VersionLow);
	EntryPoint = _EntryPoint;

	unsigned int Flag = 0;

#ifdef _DEBUG
	Flag = D3D10_SHADER_DEBUG;

#endif
	Flag |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;

	COMPTR<ID3DBlob> Error;
	std::wstring UniCodePath = Base_String::AnsiToUniCode(_Path);
	if (S_OK != D3DCompileFromFile(UniCodePath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, EntryPoint.c_str(), Version.c_str(), Flag, 0, BinaryCode.GetAddressOf(), Error.GetAddressOf()))
	{
		// 에러를 텍스트로 출력
		std::string ErrorString = reinterpret_cast<char*>(Error->GetBufferPointer());
		MsgAssert(ErrorString);
		return;
	}

	// 컴파일된 바이트코드로 GPU용 Vertex Shader 생성
	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateVertexShader(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), nullptr, VertexShader.GetAddressOf()))
	{
		MsgAssert("버텍스 쉐이더 핸들 생성에 실패");
		return;
	}

	// 상수버퍼 세팅, 리소스 세팅
	ShaderResourceSetting();
}

/////////////////////////////////////// Ext_DirectXPixelShader
Ext_DirectXPixelShader::Ext_DirectXPixelShader()
{
	Type = ShaderType::Pixel;
}

void Ext_DirectXPixelShader::CreatePixelShader(std::string_view _Path, std::string_view _EntryPoint, UINT _VersionHigh, UINT _VersionLow)
{
	CreateVersion("ps", _VersionHigh, _VersionLow);
	EntryPoint = _EntryPoint;

	unsigned int Flag = 0;

#ifdef _DEBUG
	Flag = D3D10_SHADER_DEBUG;

#endif
	Flag |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR;

	COMPTR<ID3DBlob> Error;
	std::wstring UniCodePath = Base_String::AnsiToUniCode(_Path);
	if (S_OK != D3DCompileFromFile(UniCodePath.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, EntryPoint.c_str(), Version.c_str(), Flag, 0, BinaryCode.GetAddressOf(), Error.GetAddressOf()))
	{
		// 에러를 텍스트로 출력
		std::string ErrorString = reinterpret_cast<char*>(Error->GetBufferPointer());
		MsgAssert(ErrorString);
		return;
	}

	// 컴파일된 바이트코드로 GPU용 Pixel Shader 생성
	if (S_OK != Ext_DirectXDevice::GetDevice()->CreatePixelShader(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), nullptr, PixelShader.GetAddressOf()))
	{
		MsgAssert("픽셀 쉐이더 핸들 생성에 실패");
		return;
	}

	// 상수버퍼 세팅, 리소스 세팅
	ShaderResourceSetting();
}

/////////////////////////////////////// Ext_DirectXShader
void Ext_DirectXShader::CreateVersion(std::string_view _ShaderType, UINT _VersionHigt /*= 5*/, UINT _VersionLow /*= 0*/)
{
	// vs_5_0
	Version += _ShaderType;
	Version += "_";
	Version += std::to_string(_VersionHigt);
	Version += "_";
	Version += std::to_string(_VersionLow);
}

// EntryPoint 기준으로, 셰이터 종류를 탐색
ShaderType Ext_DirectXShader::FindShaderType(std::string_view _EntryPoint)
{
	std::string Lower = _EntryPoint.data();

	if (Lower.find("_VS") != std::string::npos) return ShaderType::Vertex;
	if (Lower.find("_PS") != std::string::npos) return ShaderType::Pixel;
	if (Lower.find("_CS") != std::string::npos) return ShaderType::Compute;
	if (Lower.find("_GS") != std::string::npos) return ShaderType::Geometry;
	// if (Lower.find("_HS") != std::string::npos) return ShaderType::Unknown; // 또는 Hull
	// if (Lower.find("_DS") != std::string::npos) return ShaderType::Unknown; // 또는 Domain

	return ShaderType::Unknown;
}

// 셰이더 종류 기준으로 컴파일 진행
void Ext_DirectXShader::ShaderAutoCompile(std::string_view _Path, std::string_view _EntryPoint)
{
	ShaderType Type = FindShaderType(_EntryPoint);

	switch (Type)
	{
	case ShaderType::Vertex: Ext_DirectXVertexShader::LoadVertexShader(_Path, _EntryPoint);	break;
	case ShaderType::Pixel: Ext_DirectXPixelShader::LoadPixelShader(_Path, _EntryPoint); break;
	case ShaderType::Compute: Ext_DirectXComputeShader::LoadComputeShader(_Path, _EntryPoint); break;
	case ShaderType::Geometry: MsgAssert("Geometry 셰이더 타입은 아직 안만듬"); break;
	case ShaderType::Unknown: MsgAssert("EntryPoint 탐색 실패"); break;
	}
}

 

먼저 ShaderAutoCompile() 함수로 진입해서 해당 셰이더가 어떤 종류인지 파악하게 됩니다. Vertex Shader라면 LoadVertexShader(), Pixel Shader라면 LoadPixelShader()를 실행할 것입니다.

 

이렇게 이동하면 각자의 셰이더 종류에 따라 D3DCompileFromFile()을 실시한 뒤 Create_____Shader() 함수를 호출할 것입니다.

D3DCompileFromFile(
    UniCodePath.c_str(),                // [1] 셰이더 파일 경로
    nullptr,                            // [2] #include 시 기본 처리
    D3D_COMPILE_STANDARD_FILE_INCLUDE,  // [3] 표준 인클루드 사용
    EntryPoint.c_str(),                 // [4] 셰이더 진입 함수명
    Version.c_str(),                    // [5] 셰이더 모델 버전 ("vs_5_0" 등)
    Flag, 0,                            // [6,7] 컴파일 플래그
    BinaryCode.GetAddressOf(),         // [8] 성공 시 컴파일 결과 (바이트 코드)
    Error.GetAddressOf()               // [9] 실패 시 에러 메시지 반환
)

 

Ext_DirectXDevice::GetDevice()->Create____Shader(
    BinaryCode->GetBufferPointer(), // [1] 컴파일된 바이너리코드
    BinaryCode->GetBufferSize(),    // [2] 컴파일된 바이너리코드 사이즈
    nullptr,                        // [3] 클래스 포인터(사용안함)
    VertexShader.GetAddressOf()     // [4] 핸들값을 저장할 포인터
)

 

 

[셰이더 오토 컴파일-2]

 

기존과 다른 것은 바로 리플렉션을 활용하여 상수 버퍼를 세팅하는 과정이 추가됐다는 것입니다. 해당 과정은 Create____Shader() 이후 ShaderResourceSetting()을 통해 자동으로 진행됩니다.

 

ID3D11ShaderReflection* CompileInfo = nullptr;

if (S_OK != D3DReflect(
BinaryCode->GetBufferPointer(), // [1] 컴파일된 HLSL 셰이더의 시작 주소
BinaryCode->GetBufferSize(),    // [2] 바이트코드의 크기
IID_ID3D11ShaderReflection,     // [3] 리플렉션 인터페이스 ID, ID는 __uuidof(ID3D11ShaderReflection)를 넣어도 되지만, IID_ID3D11ShaderReflection 플래그를 넣어도 동일하게 동작
reinterpret_cast<void**>(&CompileInfo))) // [4] 값을 전달받을 포인터
{
	MsgAssert("쉐이더 리플렉션에 실패했습니다.");
	return;
}

 

위의 함수 실행을 성공했다면 HLSL 셰이더 바인딩 슬롯에 정의된 리소스(지금은 상수버퍼만 있으니 cbuffer)의 이름, 크기, 바인딩 포인트 등을 런타임에 파악해줄 수 있습니다. 

void Ext_DirectXShader::ShaderResourceSetting()
{
	if (nullptr == BinaryCode)
	{
		MsgAssert("쉐이더가 컴파일 되지 않아서 쉐이더의 리소스를 조사할 수 없습니다.");
		return;
	}

	// Reflection 
	// RTTI의 비슷한 개념으로 
	ID3D11ShaderReflection* CompileInfo = nullptr;

	if (S_OK != D3DReflect(BinaryCode->GetBufferPointer(), BinaryCode->GetBufferSize(), IID_ID3D11ShaderReflection, reinterpret_cast<void**>(&CompileInfo)))
	{
		MsgAssert("쉐이더 리플렉션에 실패했습니다.");
		return;
	}

	D3D11_SHADER_DESC Info;
	CompileInfo->GetDesc(&Info);
	D3D11_SHADER_INPUT_BIND_DESC ResDesc;

	// 내가 사용한 상수버퍼 텍스처 샘플러등의 총합입니다.
	for (UINT i = 0; i < Info.BoundResources; i++)
	{
		// 리소스 정보를 얻어오게 되고
		CompileInfo->GetResourceBindingDesc(i, &ResDesc);
		std::string Name = ResDesc.Name;
		D3D_SHADER_INPUT_TYPE Type = ResDesc.Type;
		std::string UpperName = Base_String::ToUpper(ResDesc.Name);

		switch (Type)
		{
		case D3D_SIT_CBUFFER:
		{
			ID3D11ShaderReflectionConstantBuffer* CBufferPtr = CompileInfo->GetConstantBufferByName(ResDesc.Name);

			D3D11_SHADER_BUFFER_DESC BufferDesc;
			CBufferPtr->GetDesc(&BufferDesc);

			std::shared_ptr<Ext_DirectXConstantBuffer> ConstantBuffer = Ext_DirectXConstantBuffer::CreateConstantBuffer(UpperName, BufferDesc, BufferDesc.Size);

			ConstantBufferSetter Set;
			Set.OwnerShader = GetSharedFromThis<Ext_DirectXShader>();
			Set.Name = UpperName;
			Set.BindPoint = ResDesc.BindPoint;
			Set.ConstantBuffer = ConstantBuffer;

			BufferSetter.InsertConstantBufferSetter(Set);

			break;
		}
		// ...
	}
}

 

Info.BoundResources를 통해 셰이더 내부의 리소스 갯수 만큼 반복문을 돕니다. ResDesc.Name으로는 이름을 알아낼 수 있고, ResDesc.Type는 상수 버퍼 종류가 무엇인지를 알아냅니다.

 

이러면 Type에 따라 switch문으로 들어가서 종류에 맞게 세팅을 진행합니다. 초기 값은 대부분 Null값을 넣어줍니다. 현재 상태에서는 Vertex Shader에 TransformData를 바인딩 해뒀기 때문에 D3D_SIT_CBUFFER로 들어갈 것입니다.

 

내부에서는 상수 버퍼를 생성(CreateBuffer())하고, ConstantBufferSetter라는 자료형에 값을 넣어주고 있습니다. ConstantBufferSetter는 그냥 유동적으로 상수 버퍼를 세팅하기 위해 해당 프레임워크에서 따로 만들어둔 클래스입니다. 내부는 아래와 같이 되어 있습니다.

#pragma once

// 상수버퍼 저장용 인터페이스
struct ConstantBufferSetter
{
	std::string Name;
	std::weak_ptr<class Ext_DirectXShader> OwnerShader;
	int BindPoint = -1; // b0 t0 같은 몇번째 슬롯에 세팅되어야 하는지에 대한 정보.
	std::shared_ptr<class Ext_DirectXConstantBuffer> ConstantBuffer;
	const void* CPUData;
	UINT CPUDataSize;

	void Setting();
};

// 다양한 종류의 상수 버퍼를 저장하고 가져오기 위해 사용하는 클래스
class Ext_DirectXBufferSetter
{
	friend class Ext_MeshComponentUnit;

public:
	// constrcuter destructer
	Ext_DirectXBufferSetter();
	~Ext_DirectXBufferSetter();

	// delete Function
	Ext_DirectXBufferSetter(const Ext_DirectXBufferSetter& _Other) = delete;
	Ext_DirectXBufferSetter(Ext_DirectXBufferSetter&& _Other) noexcept = delete;
	Ext_DirectXBufferSetter& operator=(const Ext_DirectXBufferSetter& _Other) = delete;
	Ext_DirectXBufferSetter& operator=(Ext_DirectXBufferSetter&& _Other) noexcept = delete;

	// 생성된 상수버퍼 저장하기
	void InsertConstantBufferSetter(const ConstantBufferSetter& _Setter) { ConstantBufferSetters.insert(std::make_pair(_Setter.Name, _Setter)); } 

	// 상수버퍼 데이터 저장
	template<typename Type>
	void SetConstantBufferLink(std::string_view _Name, const Type& _Data)
	{
		SetConstantBufferLink(_Name, reinterpret_cast<const void*>(&_Data), sizeof(Type));
	}

protected:
	
private:
	std::multimap<std::string, ConstantBufferSetter> ConstantBufferSetters; // 상수 버퍼 저장용 컨테이너
	
	void SetConstantBufferLink(std::string_view _Name, const void* _Data, UINT _Size); // 상수버퍼 데이터 저장
	void Copy(const Ext_DirectXBufferSetter& _OtherBufferSetter); // 버퍼 세팅 복사/붙여넣기 실시
	void BufferSetting(); // 버퍼 세팅 호출
};

 

나중에 다양한 렌더링 파이프라인을 활용할 때, 해당 클래스를 통해 쉽게쉽게 바인딩해서 사용할 수 있도록 해뒀습니다. 이 부분은 나중에 실제로 사용할 때 따로 설명드리도록 하겠습니다.

[목차]

 

- 중요한 요

- 현재의 문제

- 카메라 생성

- 뷰 행렬

- 프로젝션 행렬

- 뷰포트 행렬

- 결과 적용

 

 

[중요한 요약]

 

 

 

[현재의 문제]

 

이전 단계에서 정사각형을 출력하고 싶었지만, 직사각형이 출력되는 문제가 있었습니다.

 

문제의 상황에 대해 다시 설명드리자면, 화면의 종횡비와 NDC(Normalized Device Coordinates) 좌표 공간 차이로 인해 발생하는 것입니다. Direct3D에서 정점 셰이더까지의 최종 좌표는 NDC로 변환되고, 좌표계는 각각 다음의 범위를 가집니다.

범위
X -1.0 ~ 1.0
Y -1.0 ~ 1.0
Z 0.0 ~ 1.0(혹은 -1.0 ~ 1.0)

 

하지만 윈도우창은 1280x720로 생성되어 16:9의 종횡비를 가져, 가로가 세로보다 1.78배 더 긴 상태입니다. 때문에 실제 픽셀 매핑 시(ViewPort 변환 시) NDC의 X축 범위(-1.0 ~ 1.0)은 1280픽셀, Y축 범위(-1.0 ~ 1.0)는 720픽셀에 분배됩니다. 이로 인해 정사각형이 실제 화면에서는 가로로 늘어난 직사각형처럼 보이게 됩니다.

 

 

[카메라 생성]

 

해당 문제를 해결하기 위해서는 뷰, 프로젝션 행렬을 만들어서 사용해야합니다. 뷰포트 행렬은 기존에 RenderTarget에 정의한 것을 그대로 쓸 것입니다.

뷰(View) - "시점"의 기준을 정의하는 행렬
- 뷰 스페이스 생성
프로젝션(Projection) - 3D 물체를 2D 화면으로 투영하여 왜곡을 보정해주는 행렬
- Clip 스페이스 생성
뷰포트(Viewport) - NDC 좌표를 실제 화면 픽셀 좌표로 변환하기 위한 행렬
- 이건 이전에 RenderTarget 만들 때 이미 만들었음

 

뷰, 프로젝션 행렬을 생성하기 위해서는 기준이 하나 필요합니다. 이를 위해 카메라라는 것을 생성했습니다. 해당 프레임워크에서 카메라는 Ext_Actor 클래스를 상속받으며, 내부에 Ext_Transform을 가지고 있습니다. 최초 생성 시에 따로 설정하지 않으면 위치 { 0.0f, 0.0f, 0.0f }, 회전 { 0.0f, 0.0f, 0.0f }, 크기 { 1.0f, 1.0f, 1.0f }의 데이터 값을 갖습니다. 이제 이 좌표를 기준으로 RenderTarget에 출력 결과물들이 그려질 것입니다.

 

 

[뷰 행렬(View Matrix)]

 

뷰 행렬은 월드 공간에 존재하는 모든 객체들을 카메라 기준으로 재배치해주는 행렬입니다. 3D상의 물체는 위치속성 뿐만 아니라 회전 속성도 보유하고 있습니다.

 

카메라가 (0, 0, 0)에 있고, 캐릭터가(0, 0, 100)에 위치해있으며, 카메라 시점이 30도 회전한 상태라고 가정해보겠습니다.

 

뷰 행렬은 여기서 카메라가 30도 회전한 것으로 만드는 것이 아니라, 카메라는 { 0, 0, 0 }에 위치시키고 회전값도 { 0, 0, 0 }으로 만들면서 나머지 물체들을 그만큼 이동시키는 것입니다.

 

 

행렬의 생성은 DirectX의 XMMatrixLookToLH() 함수를 활용합니다.

DirectX::XMMatrixLookToLH(_EyePos, _EyeDir, _EyeUp);

 

이 함수는 카메라의 위치와 시선 방향, 위쪽 방향을 입력 받아 뷰 행렬을 생성하는 함수입니다.

FXMVECTOR EyePosition 카메라의 위치
FXMVECTOR FocusPosition 바라보는 위치
FXMVECTOR UpDirection 카메라의 위 방향(보통 Y축으로 설정)

 

함수 내부에서는 다음의 동작이 수행되면서 행렬이 생성됩니다.

float4 EyeDir = _EyeDir.NormalizeReturn();        // 카메라 정면 (+Z 방향)
float4 EyeUp = _EyeUp.NormalizeReturn();          // 월드 업 벡터
float4 EyeRight = float4::Cross3DReturn(EyeUp, EyeDir).NormalizeReturn(); // 오른쪽

ArrVector[0] = EyeRight;  // X축
ArrVector[1] = EyeUp;     // Y축
ArrVector[2] = EyeDir;    // Z+축 (정면)

// 카메라 위치를 원점으로 이동시키기 위한 보정값 생성
float4 NegEyePos = -_EyePos;
float D0Value = float4::DotProduct3D(EyeRight, NegEyePos);
float D1Value = float4::DotProduct3D(EyeUp,    NegEyePos);
float D2Value = float4::DotProduct3D(EyeDir,   NegEyePos);

// 행렬 전치 (Row → Column)
Transpose();

// 마지막 행 = 위치
ArrVector[3] = { D0Value, D1Value, D2Value, 1.0f };

//////////////////////////////////////////////////////////////////////
////// Transpose 전 : 열(row) 기준
| R.x  R.y  R.z  0 |
| U.x  U.y  U.z  0 |
| F.x  F.y  F.z  0 |
| Tx   Ty   Tz   1 |

////// Transpose 후 : 행(Column) 기준
| R.x  U.x  F.x  Tx |
| R.y  U.y  F.y  Ty |
| R.z  U.z  F.z  Tz |
|  0    0    0    1 |

 

여기서 보정값 생성에 내적을 쓰는 이유는 해당 축 방향으로 얼마만큼 이동시켜야하는 것을 계산하는 것으로, 카메라의 위치를 원점에 맞추는데 필요한 축 방향의 투영 거리를 의미합니다. 

 

해당 프로젝트에서는 각 인자로 다음의 값을 전달했습니다.

//////////////////// Ext_Camera.cpp의 CameraTransformUpdate() 함수 내부
float4 EyeDir = GetTransform()->GetLocalForwardVector();
float4 EyeUp = GetTransform()->GetLocalUpVector();
float4 EyePos = GetTransform()->GetWorldPosition();

ViewMatrix.LookToLH(EyePos, EyeDir, EyeUp);

//////////////////// Ext_Transform.h
float4 GetLocalForwardVector() { return TFData->WorldMatrix.ArrVector[2].NormalizeReturn(); }
float4 GetLocalUpVector() { return TFData->WorldMatrix.ArrVector[1].NormalizeReturn(); }
float4 GetLocalRightVector() {	return TFData->WorldMatrix.ArrVector[0].NormalizeReturn(); }

//////////////////// float4.h(Base_Math)
void Normalize()
{
	DirectVector = DirectX::XMVector3Normalize(*this);
}

// 벡터(float4)를 단위벡터로 변경후 리턴
float4 NormalizeReturn()
{
	float4 Result = *this;
	Result.Normalize();
	return Result;
}

 

EyePos는 카메라 현재 위치, EyeDir는 +Z축(전방), EyeUp은 +Y축(위)으로 전달했습니다. 이러면 카메라가 바라보는 시점으로 좌표계가 정의되면서 3D 카메라 공간(View Space)이 구축됩니다.

 

아래는 생성된 View Space에 대한 좌표값이 어떤 것을 의미하는지 나타내는 표입니다.

+X 오른쪽
-X 왼쪽
+Y
-Y 아래
+Z
-Z

 

 

[프로젝션 행렬(Projection Matrix)]

 

뷰 행렬을 활용하면 Object들이 View Space에 정렬된 상태가 됩니다. 하지만 결국 모니터에 띄워야하는 화면은 2D 공간이기 때문에 3D 좌표값으로는 출력할 수가 없습니다. 따라서 View Space 좌표값을 2D 좌표값으로 투영시키기 위한 행렬이 필요합니다. 여기서 쓰이는 행렬이 바로 프로젝션 행렬입니다. 프로젝션 행렬이 적용되면 Object들은 Clip Space에 압축된 좌표값을 갖게 됩니다.

 

프로젝션의 방식은 직교(Othology)와 원근(Perspective)가 있는데, 지금은 원근 투영만 알아보도록 하겠습니다. 원근 투영 행렬의 생성은 DirectX의 XMMatrixPerspectiveFovLH() 함수를 활용합니다.

DirectX::XMMatrixPerspectiveFovLH(_FovAngle, _AspectRatio, _NearZ, _FarZ);
FovAngle - 수직 시야각(Field of View; FOV)으로, 단위는 라디안(Radian)을 넣어줘야함
- 현재 프로젝트에서는 수직이 Y축
- 3D 렌더링 시스템은 AspectRatio(해상도)가 가변적이기 때문에 수직 시야각을 사용하고 가로 시야각은 종횡비로 보정하는게 일관성 유지에 유리하기 때문
float Aspect - 종횡비(가로/세로)
- ScreenWidth / ScreenHeight
float NearZ - 가까운 클리핑 거리(Near Plane), 근평면이라고 함
- 이 거리보다 가까우면 렌더링에서 제외
float FarZ - 먼 클리핑 거리(Far Plane), 원평면이라고 함
- 이 거리보다 멀면 렌더링에서 제외

 

함수 내부에서는 다음의 동작이 수행되면서 행렬이 만들어집니다.

float FovRad = _FovAngle * GameEngineMath::DegToRad; // 라디안으로 값 변환
float YScale = 1.0f / tanf(FOV / 2.0f); // y축 시야각이 클수록 값이 작아짐 → 멀리 보임
float XScale = YScale / _AspectRatio; // 가로 시야는 종횡비에 따라 정해짐
float ZRange = _FarZ - _NearZ; // Z 거리 범위 (Far - Near)
float Zn = _NearZ;
float Zf = _FarZ;

Arr2D[0][0] = XScale; // [1]
Arr2D[1][1] = YScale; // [2]
Arr2D[2][2] = Zf / ZRange; // [3]
Arr2D[2][3] = 1.0f; // [4]
Arr2D[3][2] = -Zn * Zf / ZRange; // [5]
Arr2D[3][3] = 0.0f; // [6]
// [1] X축 스케일링 → NDC 공간에서 좌우 시야 조절
// [2] Y축 스케일링 → NDC 공간에서 상하 시야 조절
// [3] Z값 정규화 (0~1로 변환하기 위한 깊이 매핑), FarZ에 가까울수록 값이 1에 가까워짐
// [4] 원근 분할을 위해 Z 값을 W로 복사 → 투영 후 divide 수행 가능하게 설정
// [5] 정규화된 Z값 계산을 위한 보정 항, -Zn * Zf / (Zf - Zn) 형태는 원근 보정된 깊이 값 계산에 사용됨
// [6] 투영 행렬 마지막 요소: 0, W 성분을 Z로부터 받아야 하므로 이 위치는 0으로 둬야 함

//////////////////////////////////////////////////////////////////////
////// 생성된 열(row) 기준 원근 투영 행렬
| XScale    0         0             0 |
|   0     YScale      0             0 |
|   0       0      Zf/(Zf-Zn)       1 |
|   0       0   -Zn*Zf/(Zf-Zn)      0 |

 

XScale, YScale, Zf/(Zf-Zn)으로 인해 Object의 X, Y값은 -1.0 ~ 1.0, Z값은 0.0 ~ 1.0으로 압축됩니다. 원근 투영은 헷갈리는 부분이 많기 때문에, 하나씩 차근차근 알아보도록 하겠습니다.

 

1. YScale

: 공식은 [float YScale = 1.0f / tanf(FOV / 2.0f)]입니다. 그림을 통해 알아보겠습니다.

 

같은 크기의 Object(파란색 사각형)일지라도 멀리 있을 수록 더 작게 보여야 합니다. 이때 Z값이 증가하면 그림과 같이 X, Y값도 증가하게 됩니다(뒤에 있을수록 약간씩 왼쪽으로 이동함). 이때 사각형의 위치는 탄젠트(tan)로 구할 수 있습니다.

// 탄젠트는 밑변에 대한 높이의 비
tan(θ) = 반대변 / 인접변
tan(θ) = (Y의 길이) / (Z의 길이)
tan(FOV / 2) = y / z

따라서, y = z * tan(FOV / 2)

 

+) FOV / 2인 이유는 그림에서 보시는 것과 같이 전체 FOV값에서 절반만 활용해서 값을 구하기 때문입니다.

 

이제 이 값을 원근 투영 행렬에 변환하기 위해 사용하는 것이 YScale = 1 / tan(FOV / 2) 입니다. y값을 압축해서 -1 ~ 1 범위로 맞추는 스케일 계수인 것입니다.

 

2. XScale

: X축 시야각은 Y축과 동일하게 구할 수 있지만, 추가로 종횡비를 나눠줍니다. 3D 렌더링 시스템은 AspectRatio(해상도)가 가변적이기 때문에 Y시야각(세로 시야각)을 고정으로 구하고 X시야각(가로 시야각)은 종횡비로 보정하기 때문입니다.

XScale = 1 / (tan(FovY / 2) * AspectRatio)
       = (1 / tan(FovY / 2)) / AspectRatio
       = YScale / AspectRatio

 

3. Zf/(Zf - Zn) & -Zn * Zf / (Zf - Zn) & Arr2D[2][3] = 1.0f

: Z값은 0.0 ~ 1.0 사이 값으로 압축해야 합니다. 하지만 이걸 직선적으로 매핑해버리면 원근 효과가 사라지게 됩니다. 그래서 원근 투영 행렬에서는 아래와 같이 비선형적으로 변환해줍니다.

z_clip = A * z + B       ← 투영 행렬의 z 변환 결과
w_clip = z               ← 투영 행렬의 w 성분
z_ndc  = z_clip / w_clip = (A * z + B) / z

따라서 z_ndc = A + (B / z)   ← 원근 분할 (Perspective Divide)

 

이렇게 나온 z_ndc 값으로 연립 방정식을 풀면 A와 B 값을 알 수 있습니다, NearZ일 경우에는 0, FarZ일 경우에는 1을 넣어서 풀어보면 됩니다.

//// NearZ의 경우
A + (B / Zn) = 0
→ A = -B / Zn

//// FarZ의 경우
A + (B / Zf) = 1
→ (-B / Zn) + (B / Zf) = 1
→ B * (-1/Zn + 1/Zf) = 1
→ B = 1 / ( -1/Zn + 1/Zf ) = (Zn * Zf) / (Zf - Zn)

 

이러면 A값은 다음과 같습니다.

A = -B / Zn = - (Zf) / (Zf - Zn)

따라서 A(Projection[2][2]) = Zf / (Zf - Zn)

 

4. Arr2D[3][3] = 0.0f

: w 성분 계산 시의 상수 항으로, w_clip = z 값을 보존하기 위해(이외에는 아무것도 더해지지 않도록) 0.0f 값을 갖습니다.

 

5. 예시

: FOV가 60도, 16:9 해상도, NearZ = 0.1, FarZ = 1000이 입력된 예시를 확인해보도록 하겠습니다.

float FovY = XMConvertToRadians(60.f); // 1.0472
float Aspect = 16.0f / 9.0f;
float NearZ = 0.1f;
float FarZ = 1000.0f;

float yScale = 1.0f / tanf(FovY / 2);       // ≈ 1.732
float xScale = yScale / Aspect;             // ≈ 0.974
float zCompress = FarZ / (FarZ - NearZ);    // ≈ 1.0001
float zBias = -NearZ * FarZ / (FarZ - NearZ); // ≈ -0.10001

// 결과 행렬:
| 1.732    0       0        0 |
| 0        0.974   0        0 |
| 0        0       1.0001   1 |
| 0        0      -0.10001  0 |

 

이제 View Space에 있는 점 ViewP(2, 3, 10, 1)에 이 행렬을 적용해봅시다. ProjectionMatrix * ViewP를 실시하면 원근 투영이 적용되어 Clip Space로 이동됩니다(ViewP -> ClipP).

 

- ClipX : x * XScale = 2 * 0.974 = 1.948

- ClipY : y * YScale = 3 * 1.732 = 5.196

- ClipZ : z * (Z압축 + Z보정) = 10 * 1.0001 + (-0.10001) = 10.001 - 0.10001 = 9.901

- ClipW : z = 10

 

이제 이것을 NDC 공간으로 옮기게 되는데, 이 때는 W Divide(W 나누기)를 수행해줍니다.

 

- NDC.x = ClipX / ClipW = 1.948/10 = 0.1948
- NDC.y = ClipY / ClipW = 5.196/10 = 0.5196
- NDC.z = ClipZ / ClipW = 9.901/10 = 0.9901

- NDC.w = ClipW / ClipW = 10/10 = 1

 

해당 단계는 Rasterizer에서 수행됩니다. W Divide가 수행되기 때문에 Z값이 크면 클수록(시점에서 멀어지면 멀어질수록) 크기가 작아지게 됩니다.

 

Clip Space 정점들이 다음의 값을 갖는다고 가정해봅시다.

 

- A: (2, 2, 2, 2)

- B : (2, 2, 10, 10)

- C : (2, 2, 50, 50)

 

이러면 A는 이후 2로, B는 10으로, C는 50으로 각 요소를 나눠줘야합니다. 

 

- NDCA : (1.0, 1.0, 1.0)

- NDCB : (0.2, 0.2, 0.2)

- NDCC : (0.04, 0.04, 0.04)

 

확인할 수 있는 것과 같이, X, Y 크기가 같아도 Z값이 다르면 크기가 달라지는 것을 확인할 수 있습니다.

 

 

[뷰포트 행렬]

 

프로젝션 행렬을 통해 Clip Space 좌표계로 값이 변경된 후, Rasterizer에서 W 나누기를 수행하면 NDC 좌표계로 값이 변경됩니다. 이제 이 Object 좌표값을 픽셀 공간(Screen Space)로 매핑해주기 위해 뷰포트 행렬을 활용합니다.

 

하지만 DirectX11에서는 뷰포트 행렬을 구하는 함수를 따로 지원하지 않습니다. 왜냐하면 하드웨어 고정 기능으로 처리하기 때문입니다. 우리는 이것을 RenderTarget을 만들 때 이미 정의해줬으며, 바인딩은 RSSetViewports() 함수를 호출하여 수행해줬습니다.

void Ext_DirectXRenderTarget::RenderTargetSetting()
{
	COMPTR<ID3D11RenderTargetView> RTV = Textures[0]->GetRTV(0);

	if (nullptr == RTV)
	{
		MsgAssert("랜더타겟 뷰가 존재하지 않아서 클리어가 불가능합니다.");
	}

	COMPTR<ID3D11DepthStencilView> DSV = DepthTexture->GetDSV();

	if (nullptr == DSV)
	{
		MsgAssert("뎁스스텐실뷰 왓");
	}
	
	Ext_DirectXDevice::GetContext()->OMSetRenderTargets(static_cast<UINT>(RTVs.size()), RTV.GetAddressOf(), DSV.Get());
	Ext_DirectXDevice::GetContext()->RSSetViewports(static_cast<UINT>(ViewPorts.size()), &ViewPorts[0]);
}

 

앞서 정의해준 뷰포트 값은 변환 수식에서 다음과 같이 사용됩니다.

ScreenX = (NDC.x + 1.0f) * 0.5f * Viewport.Width  + Viewport.TopLeftX;
ScreenY = (1.0f - NDC.y) * 0.5f * Viewport.Height + Viewport.TopLeftY;
ScreenZ = NDC.z * (MaxDepth - MinDepth) + MinDepth;
Viewport.Width, Height 실제 렌더링할 영역 크기(픽셀 단위)
TopLeftX, TopLeftY 뷰포트 시작 위치(기본 0, 0)
MinDepth, MaxDepth 0.0 ~ 1.0(깊이 버퍼 범위

 

 

[결과 적용]

 

세 가지 행렬을 모두 정의해줬다면, 이 값들을 구해줘야합니다. 해당 프레임워크에서는 Rendering() 함수를 호출하기 전에 Ext_Scene 클래스의 Update 함수에서 구해줬습니다.

// 현재 카메라 타입에 따라 뷰, 프로젝션, 뷰포트 행렬 세팅
void Ext_Camera::CameraTransformUpdate()
{
	float4 EyePos = GetTransform()->GetWorldPosition();
	float4 EyeDir = GetTransform()->GetLocalForwardVector();
	float4 EyeUp = GetTransform()->GetLocalUpVector();
	ViewMatrix.LookToLH(EyePos, EyeDir, EyeUp);

	switch (CameraType)
	{
	case ProjectionType::Perspective:
	{
		ProjectionMatrix.PerspectiveFovLH(FOV, Width / Height, Near, Far);
		break;
	}
	case ProjectionType::Orthogonal:
	{
		// Projection.OrthographicLH(Width * ZoomRatio, Height * ZoomRatio, Near, Far); 일단 패스
		break;
	}
	case ProjectionType::Unknown:
	{
		MsgAssert("카메라 투영이 설정되지 않았습니다.");
		break;
	}
	}

	ViewPortMatrix.ViewPort(Width, Height, 0.0f, 0.0f);
}

 

이제 렌더링될 물체들의 Transform에 행렬들을 적용해줍니다.

void Ext_Camera::Rendering(float _Deltatime)
{
	// MeshComponents 렌더링 업데이트 시작
	for (auto& [Key, MeshComponentList] : MeshComponents)
	{
		// [!] 필요하면 ZSort 실시(나중에)

		for (const std::shared_ptr<Ext_MeshComponent>& CurMeshComponent : MeshComponentList)
		{
			if (!CurMeshComponent->GetIsUpdate()) continue;
			else
			{
				CurMeshComponent->Rendering(_Deltatime, GetSharedFromThis<Ext_Camera>()); // [3] 현재 MeshComponent에게 카메라의 View, Projection 곱해주기
				// [!] 필요하면 픽셀 셰이더에서 활용할 Value들 업데이트
			}
		}

	}
    
    //...
}

 

프레임워크에서 위의 호출 구조는 객체화된 MeshComponent의 TransformData에 값을 전달하도록 설계되어 있습니다. 함수를 계속 따라 들어가면, 아래의 함수가 호출됩니다.

// 카메라 기준으로 월드, 뷰, 프로젝션 행렬 생성
void TransformData::SetViewProjectionMatrix(const float4x4& _View, const float4x4& _Projection)
{
    ViewMatrix = _View;
    ProjectionMatrix = _Projection;
    WorldViewMatrix = WorldMatrix * ViewMatrix;
    WorldViewProjectionMatrix = WorldMatrix * ViewMatrix * ProjectionMatrix;
}

 

사실 View, Projection Matrix를 단순하게 전달만 하고 Vertex Shader에서 연산 처리를 실시해도 되지만, 굳이 이런 식으로 연산한 이유는 값을 따로 확인해보기 위함입니다.

[목차]

 

- 기능추가 2가 진행된 이유

- 프로젝트 구성

- 프레임워크 Flow

- 추가된 기능들

 

 

[기능추가 2가 진행된 이유]

 

단순하게 하나의 결과물만 출력한다면 이전 포스팅까지 진행했던 방식대로 하나씩 출력해보면 되지만, 궁극적으로는 게임 프레임워크가 어떻게 이루어져있는가와 더불어 그래픽스 학습을 위해 만들고 있는 프레임워크이기 때문에(물론 이 프레임워크로 게임을 제작하지 않을 수도 있습니다. 이외에 할 게 많기 때문입니다) 렌더 결과물을 하나씩 출력하거나, 출력 결과물마다 렌더링 파이프라인을 일일이 설정해줄 수 없는 노릇이었습니다.

 

이에 따라 필요한 기능들을 추가했고, 다음과 같은 결과를 얻을 수 있었습니다.

아마 25.05.22~23 쯤
일주일 후, 25.05.28~29일쯤

 

 

[프로젝트 구성]

 

추가된 기능들을 간단하게 살펴보기에 앞서, 현재 프레임워크가 어떻게 이루어져 있는지, 어떤 흐름을 가지는지에 대해 간략하게 설명드리고 넘어가겠습니다. 프레임워크의 프로젝트 구성은 다음과 같습니다.

DirectX11_Base - 가장 상위 프로젝트
- 프레임워크에 기본적으로 필요한 기능들을 담아둠(윈도우창 생성, float4 자료형, 델타타임 등)
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_Extension - 프레임워크 Loop간 필요한 기능과 DirectX 관련 기능 모음 프로젝트
- 사실 이름을 Extention이 아니고 Engine이라고 지었어야 하지 않았을까?
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_Contents - 사용자를 위한 프로젝트
- 여기서 Actor, Scene, gui 등 자유롭게 만들고 테스트하면 됨
- 빌드하면 정적 라이브러리(,lib)가 됨
DirectX11_RenderingPipeline - 시작 프로젝트
- exe 파일로 빌드되어 이걸로 실행하면 프로세스 동작
Shader - 필요한 셰이더 목록
ThirdPaty - DirectXTK : 텍스쳐 로드용
- assimp : 메시 로드용
- imgui : 사실 여기 있어야하는데, 그냥 Extension 프로젝트에 넣어놨다.

 

+) 사실 DirectX11_Extension 프로젝트의 경우, DirectX11_Engine이라는 이름이 맞습니다만, 작명 실수입니다. 이걸 고치기에는 너무 먼 길을 와버려서 그냥 넘어가기로 했습니다.

 

 

[프레임워크 Flow]

 

프레임워크의 전체적인 흐름은 다음과 같습니다.

Window Create - exe 파일 시작 시, 윈도우창 생성
- 생성 즉시 Window Loop 진입
Level(Scene) Create - Engine Start 부분에서 필요한 Scene이 있다면 Create 실시
- Scene이 생성됨과 동시에 Ening Core에 저장
- 여기서 Actor랑 Component 생성해도 됨
Program Loop Check - 윈도우창이 파괴될 경우 프로세스 종료로 진입, 아니라면 루프 진행
Level(Scene)
Change Check
- Scene이 변경됐는지 체크, 변경됐다면 해당 레벨의 SceneChangetStart를 실시하고, 이전 Scene은 SceneChangeEnd를 실시하여 설정 변경
- Scene 변경 시 필요한 Actor, Component 생성 및 저장
- 생성된 Actor와 Component는 Window Loop에서 Update 실시
Deltatime Calculation - Deltatime을 산출
- 현재 프로세스는 60FPS로 제한된 상태(따로 설정함)
Level(Scene) Update - 선택된 Scene의 Update 실시
Actor Update - 선택된 Scene에 존재하는 Actor와 Component의 Update 실시
- 이동, 피격(아직 안함) 등 설정, 이외 업데이트 시 실행할 동작들을 정의(카메라 포함)
Rendering Setting - 렌더 타겟 뷰 클리어 및 세팅
- 카메라 뷰, 프로젝션, 뷰포트 행렬 계산
- Actor에 부착된 MeshComponent의 Rendering Setting 진행
- Rendering Pipeline으로 설정된 옵션별로 세팅 진행
- 이후 DrawIndexed로 렌더타겟뷰에 모두 그려줌
Present - 렌더타겟뷰를 Present하여 화면에 출력
Release - 위에는 표시 안되어있지만, 루프 한번 돈 이후 만약 Destroy를 호출한 액터나 컴포넌트가 있다면 제거
Window End - 윈도우 종료

 

+) 이후 Destroy 기능을 추가해서 Present 이후 단계에서 Release 단계를 실시합니다. 이전에 Destroy를 호출한 Actor나 Component가 있다면, Release 단계에서 지워줍니다(메모리에서 소거).

 

 

[추가된 기능들]

 

1. Scene

: 액터와 컴포넌트, 카메라를 관리하는 클래스입니다. 여러 개가 존재할 수 있으며 자신에게 포함된 액터, 컴포넌트, 카메라의 라이프사이클을 관리합니다.

 

2. Actor

: 트랜스폼을 갖는 오브젝트입니다. 이동, 회전, 크기 변경이 가능하고 컴포넌트를 탈부착하여 기능을 확장할 수 있습니다.

 

3. Component

: 액터에 탈부착하는 객체입니다. 마찬가지로 트랜스폼을 가지고, 액터에 부착되면 트랜스폼을 통해 액터의 자식으로 등록됩니다. 이러면 액터의 트랜스폼을 부모 트랜스폼으로 인식하고, 이동, 회전, 크기 변경 대해 액터의 영향을 받게 됩니다. 저의 프로젝트에서는 주로 Mesh를 위한 컴포넌트로 설계되어있는데, 충돌체나 물리 컴포넌트 등 다양한 기능들을 탈부착할 수도 있습니다(아마 안할듯..).

 

4. Transform

: 액터와 컴포넌트의 기하구조를 위한 클래스입니다. Local Matrix(로컬 행렬)과 World Matrix(월드 행렬)로 나뉘며, 부모가 있을 때와 없을 때로 나뉩니다.

> 부모가 없을 때 = 로컬 행렬이 지정되면 그건 바로 월드 행렬이 됩니다(로킬이 곧 월드).

> 부모가 있을 때 = 로컬 행렬이 지정되고 부모 월드 행렬의 영향을 받아 부모+자신으로 월드 행렬이 생성됩니다. 여기에 대해선 많은 옵션이 있을 수 있지만, 기본적으로는 이것만 만들어두었습니다(크게 쓸 일이 없을 것으로 예상됨)

 

5. Input

: 간단한 카메라 조작을 위해 Input을 관리하는 클래스를 만들었습니다. 매 프레임 시작 부분에서 GetAsyncKeyState()로 입력이 발생했는지 확인합니다. 입력이 발생했다면 눌렀는지, 누르고 있는지, 안누르는지만 확인해줍니다.

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/152996

 

[풀이]

 

시소는 2, 3, 4미터 거리에 좌석이 있고 (현재 몸무게 * 시소 거리) == (다른사람 몸무게 * 시소거리)가 같으면 시소가 평행을 이루어 시소 짝궁이 된다.

 

예를 들어, A와 B가 있고 거리 비율이 2:3이면, A * 2 = B * 3 → A/B = 3/2이 되는 방식인데, 이 때문에 7가지 조건이 성립된다.

 

1. 1:1

: A = B

answer += WeightMap[Weight];

 

2. 2:3

: A = (3/2)B

if ((Weight * 2) % 3 == 0)  // 2:3
{
	answer += WeightMap[(Weight * 2) / 3];
}

 

3 3:2

: A = (2/3)B

if ((Weight * 3) % 2 == 0) // 3:2
{
	answer += WeightMap[(Weight * 3) / 2];
}

 

4. 1:2

: A = 2B

if ((Weight % 2) == 0) // 1:2
{
	answer += WeightMap[Weight / 2];
}

 

5. 2:1

: A = B/2

answer += WeightMap[Weight * 2]; // 2:1

 

6. 3,4

: A = (4/3)B

if ((Weight * 3) % 4 == 0) // 3:4
{
	answer += WeightMap[(Weight * 3) / 4];
}

 

7. 4,3

: A = (3/4)B

if ((Weight * 4) % 3 == 0) // 4:3
{
	answer += WeightMap[(Weight * 4) / 3];
}

 

마지막으로 현재 무게를 map에 등록해 다음 사람에서 사용할 수 있게 해준다.

#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

// 시소는 중심으로부터 2(m), 3(m), 4(m) 거리의 지점에 좌석이 하나씩 있습니다.
// 시소를 두 명이 마주 보고 탄다고 할 때,
// 탑승한 사람의 무게와 시소 축과 좌석 간의 거리의 곱이 양쪽 다 같다면 시소 짝꿍
// 사람들의 몸무게 목록 weights이 주어질 때, 시소 짝꿍이 몇 쌍 존재하는지 구하여 retur
// 2 ≤ weights의 길이 ≤ 100,000
// 100 ≤ weights[i] ≤ 1,000
long long solution(vector<int> weights) 
{
    long long answer = 0;
    unordered_map<long long, long long> WeightMap;

    for (int Weight : weights)
    {
        // 같은 무게
        answer += WeightMap[Weight];
        
        if ((Weight * 2) % 3 == 0) answer += WeightMap[(Weight * 2) / 3]; // 2:3
        if ((Weight * 3) % 2 == 0) answer += WeightMap[(Weight * 3) / 2]; // 3:2
        if ((Weight % 2) == 0) answer += WeightMap[Weight / 2]; // 1:2
        
        answer += WeightMap[Weight * 2]; // 2:1
        
        if ((Weight * 3) % 4 == 0) answer += WeightMap[(Weight * 3) / 4]; // 3:4
        if ((Weight * 4) % 3 == 0) answer += WeightMap[(Weight * 4) / 3]; // 4:3

        WeightMap[Weight]++;
    }

    return answer;
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/148653

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

[풀이]

 

10^n 단위로 움직일 수 있는 엘리베이터로, 최소한의 경우의 수를 찾아야하는 문제이다. 크게 세 가지 조건만 활용하면 문제를 쉽게 풀 수 있다.

 

1. 현재 자릿수가 6 이상인 경우

: 가장 가까운 10으로 올린다. 이때 앞자리가 변경되기 때문에 올림 처리를 실시해야한다.

 

2. 현재 자릿수가 4 이하인 경우

: 가장 가까운 0으로 내린다

 

3. 현재 자릿수가 5인 경우

: 여기서 올릴지 말지를 고려해야한다. 다음 자릿수가 4 이하라면, 그냥 내려도 된다. 하지만 다음 자릿수가 5 이상이라면, 올리는 것이 더 유리할 "가능성"이 존재한다. 입력값으로 2555가 들어왔다고 생각해보자.

 

- 1의 자리 5

 > 5를 올리면 10, 내리면 0이 되지만, 앞자리수들의 변동이 발생한다.

 > 내리는 경우에는 전부 내리는데, 이러면 

   : 1의 자리 5 : 5번

   : 10의 자리 5 : 5번

   : 100의 자리 5 : 5번

   : 1000의 자리 2 : 2번

   : 총 17번 이동

 

 > 올리는 경우는 전부 올리는데, 이려면

   : 1의 자리 5 : 5번

   : 10의 자리 6(5+1) : 4번

   : 100의 자리 6(5+1) : 4번

   : 1000의 자리 2 : 3번

   : 총 16번 이동

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

void Carry(vector<int>& Arr, int index)
{
    int i = index;
    Arr[i] = 0;
    while (true)
    {
        if (i + 1 >= Arr.size())
        {
            Arr.push_back(1); // 자릿수 늘림
            break;
        }
        Arr[i + 1] += 1;

        if (Arr[i + 1] < 10)
        {
            break;
        }

        Arr[i + 1] = 0;
        ++i;
    }
}

int solution(int storey) 
{
    int answer = 0;
    vector<int> Arr;
    int Buffer = storey;
    while (Buffer != 0)
    {
        Arr.push_back(Buffer % 10);
        Buffer /= 10;
    }

    for (int i = 0; i < Arr.size(); i++)
    {
        int CurDigit = Arr[i];
        if (CurDigit >= 6)
        {
            answer += (10 - CurDigit);
            if (i < Arr.size() - 1)
            {
                Arr[i + 1] += 1;
                if (Arr[i + 1] == 10)
                {
                    Carry(Arr, i + 1);
                }
            }
            else
            {
                Arr.push_back(1); // 마지막 자릿수에서 올라가는 경우
            }
        }
        else if (CurDigit == 5) // 앞자리가 6 이상으로 바뀌면 올림, 아니면 내림
        {
            if (i + 1 < Arr.size() && Arr[i + 1] >= 5)
            {
                answer += (10 - CurDigit);
                Arr[i + 1] += 1;
                if (Arr[i + 1] == 10)
                {
                    Carry(Arr, i + 1);
                }
            }
            else
            {
                answer += CurDigit;
            }
        }
        else // 4 <= Temp
        {
            // 4는 그냥 내림
            answer += CurDigit;
        }
    }

    return answer;
}

[목차]

 

- InputLayout(버텍스 정보) 정의

- Vertex Buffer

- Index Buffer

- 버퍼 생성하기

- 셰이더 컴파일

- CreateInputLayout

- 결과 출력

- 예상치 못한 결과들

 

 

[InputLayout(버텍스 정보) 정의]

 

렌더링의 가장 기본 자원은 정점(Vertex)이고, 이걸 활용하여 프리미티브 데이터(도형의 최소 단위인 점, 선, 면)을 만들 수 있습니다. 정점에는 다양한 정보를 포함시킬 수 있는데, 이 정보들에 대한 골격을 정의해주는 것이 InputLayout입니다.

 

나중에 CreateInputLayout() 함수를 호출하여 VertexBuffer, VertexShader 정보를 통해 생성을 해줘야하지만, 시멘틱 정보에 대해서는 먼저 정의해줄 수 있습니다. 아래와 같이 함수를 호출해줍니다.

Ext_DirectXVertexData::GetInputLayoutData().AddInputLayoutDesc("POSITION", DXGI_FORMAT_R32G32B32A32_FLOAT);
Ext_DirectXVertexData::GetInputLayoutData().AddInputLayoutDesc("COLOR", DXGI_FORMAT_R32G32B32A32_FLOAT);
Ext_DirectXVertexData::GetInputLayoutData().AddInputLayoutDesc("TEXCOORD", DXGI_FORMAT_R32G32B32A32_FLOAT);
Ext_DirectXVertexData::GetInputLayoutData().AddInputLayoutDesc("NORMAL", DXGI_FORMAT_R32G32B32A32_FLOAT);

 

함수의 전달 인자로 첫 번째는 시멘틱 이름, 두 번째는 자료형을 전달해줍니다. 자료형의 경우 DXGI_FORMAT_R32G32B32A32_FLOAT를 전달해주면 시멘틱 정보 하나 당 표현하는 자료형 크기가 16byte(float4)가 됩니다. 이러면 아래의 함수가 호출됩니다.

void InputLayoutData::AddInputLayoutDesc
(
	/*1*/LPCSTR _SemanticName, 
	/*2*/DXGI_FORMAT _Format, 
	/*3*/D3D11_INPUT_CLASSIFICATION _InputSlotClass, 
	/*4*/UINT _InstanceDataStepRate, 
	/*5*/UINT _AlignedByteOffset, 
	/*6*/UINT _InputSlot,
	/*7*/UINT _SemanticIndex
)
{
	D3D11_INPUT_ELEMENT_DESC Data;

	Data.SemanticName = _SemanticName;
	Data.Format = _Format;
	Data.InputSlotClass = _InputSlotClass;
	Data.InstanceDataStepRate = _InstanceDataStepRate;
	Data.AlignedByteOffset = Offset;
	Data.InputSlot = _InputSlot;
	Data.SemanticIndex = _SemanticIndex;

	Offset += FormatSize(Data.Format);
	InputLayoutDescs.push_back(Data);
}

 

해당 함수는 D3D11_INPUT_ELEMENT_DESC 구조체에 전달받은 정보를 담고 InputLayOutDesc라는 전역 컨테이너 변수에 값을 저장하는 역할을 수행합니다. 함수에 전달된 인자들의 정보는 다음과 같습니다.

LPCSTR 
_SemanticName
- 정점 요소의 의미(Semantic)를 나타내는 문자열
- "POSITION", "COLOR", "TEXCOORD", "NORMAL" 등이 주로 사용하는 문자열
- 이것은 Vertex Shader의 입력 파라미터 순서와 일치해야함
DXGI_FORMAT
_Format
- 해당 요소의 데이터 형식을 지정
> DXGI_FORMAT_R32G32B32A32_FLOAT : float4
> DXGI_FORMAT_R32G32B32_FLOAT : float3
> DXGI_FORMAT_R32G32_FLOAT : float2
- 셰이더가 해석할 데이터 구조와 일치해야함
- 여기서는 DXGI_FORMAT_R32G32B32A32_FLOAT로 활용
D3D11_INPUT_CLASSIFICATION 
_InputSlotClass
- 정점 데이터가 정점 당(per Vetex)인지, 인스턴스 당(per Instance)인지 구분
- D3D11_INPUT_PER_VERTEX_DATA나 D3D11_INPUT_PER_INSTANCE_DATA 전달
UINT 
_InstanceDataStepRate
- InputSlotClass가 PER_VERTEX_DATA일 경우에만 사용되는 정보
- 인스턴스 몇 개마다 해당 데이터를 한 번씩 사용할지 지정하며, 보통 1을 사용(1 인스턴스 당 1개 값)
UINT
_AlignedByteOffset
- 정점 구조체 내에서 이 요소가 몇 바이트 떨어져 있는지 명시
- D3D11_APPEND_ALIGNED_ELEMENT 지정 시 자동 계산이 가능
- 수동 지정 시 수동으로 누적 오프셋을 관리해야함
- 여기서는 수동 관리를 위해 0을 할당하고 Offset으로 관리
- Offset은 각 호출마다 DXGI_FORMAT_R32G32B32A32_FLOAT 크기만큼씩 쌓일 것(16)
- 다음 호출때는 쌓인 만큼(16)에서 추가로 쌓이는 형태(32)로 관리
UINT
_InputSlot
- 어떤 버퍼 슬롯에서 데이터를 읽어올 지 지정
- IASetVertexBuffers() 함수에서 바인딩된 슬롯과 일치해야함
> 예시로 0번 슬롯은 POSITION, 1번 슬롯은 TEXTCOORD 이런 식으로
UINT 
_SemanticIndex
- 같은 SemanticName이 여러 개 있을 경우, 몇 번째인지 구분함
- TEXTCOORD0, TEXTCOORD1이 있으면 각 index는 0, 1

 

 

[Vertex Buffer]

 

정보들을 정의해줬으니, 전달받은 정보들을 어떻게 사용할지를 정의해주는 Vertex Buffer를 만들어줍니다.

////////////////////// Ext_DirectXVertexBuffer.h
#pragma once
#include "Ext_ResourceManager.h"

// 버텍스 버퍼(Vertex Buffer) 생성을 위한 클래스
class Ext_DirectXVertexBuffer : public Ext_ResourceManager<Ext_DirectXVertexBuffer>
{
	friend class Ext_DirectXMesh;

public:
	// constrcuter destructer
	Ext_DirectXVertexBuffer() {}
	~Ext_DirectXVertexBuffer() {}

	// delete Function
	Ext_DirectXVertexBuffer(const Ext_DirectXVertexBuffer& _Other) = delete;
	Ext_DirectXVertexBuffer(Ext_DirectXVertexBuffer&& _Other) noexcept = delete;
	Ext_DirectXVertexBuffer& operator=(const Ext_DirectXVertexBuffer& _Other) = delete;
	Ext_DirectXVertexBuffer& operator=(Ext_DirectXVertexBuffer&& _Other) noexcept = delete;

	// Vertex Buffer 생성 및 저장
	template<typename VertexLayout>
	static std::shared_ptr<Ext_DirectXVertexBuffer> CreateVertexBuffer(std::string_view _Name, const std::vector<VertexLayout>& _Vertexs)
	{
		std::shared_ptr<Ext_DirectXVertexBuffer> NewVertexBuffer = Ext_ResourceManager::CreateNameResource(_Name);
		NewVertexBuffer->InputLayout = std::shared_ptr<InputLayoutData>(&VertexLayout::GetInputLayoutData(), [](InputLayoutData*) {});
		NewVertexBuffer->CreateVertexBuffer(&_Vertexs[0], sizeof(VertexLayout), static_cast<UINT>(_Vertexs.size()));

		return NewVertexBuffer;
	}

	// Getter
	std::shared_ptr<class InputLayoutData> GetInputLayout() { return InputLayout; }
	COMPTR<ID3D11Buffer>& GetVertexBuffer() { return VertexBuffer; }
	UINT GetVertexSize() { return VertexSize; }
	UINT GetVertexCount() { return VertexCount; }
	UINT GetBufferSize() { return VertexBufferInfo.ByteWidth; }

protected:
	
private:
	void CreateVertexBuffer(const void* _Data, UINT _VertexSize, UINT _VertexCount); // Vertex Buffer 생성 및 저장

	std::shared_ptr<class InputLayoutData> InputLayout = nullptr;	// 생성된 입력 레이아웃 정보 저장용
	D3D11_BUFFER_DESC VertexBufferInfo = { 0, };  // 버텍스 버퍼 DESC 저장용
	COMPTR<ID3D11Buffer> VertexBuffer = nullptr;   // 버텍스 버퍼 인터페이스 저장용
	UINT VertexSize = 0;  // 버텍스 사이즈
	UINT VertexCount = 0; // 버텍스 갯수
	
	UINT Offset = 0;
};

////////////////////// Ext_DirectXVertexBuffer.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXVertexBuffer.h"
#include "Ext_DirectXDevice.h"

// 버텍스 버퍼 생성
void Ext_DirectXVertexBuffer::CreateVertexBuffer(const void* _Data, UINT _VertexSize, UINT _VertexCount)
{
	VertexSize = _VertexSize;
	VertexCount = _VertexCount;

	// D3D11_BUFFER_DESC 정보 입력
	VertexBufferInfo.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	VertexBufferInfo.ByteWidth = VertexSize * VertexCount;
	VertexBufferInfo.CPUAccessFlags = 0;
	if (0 == VertexBufferInfo.CPUAccessFlags)
	{
		VertexBufferInfo.Usage = D3D11_USAGE_DEFAULT;
	}
	else
	{
		VertexBufferInfo.Usage = D3D11_USAGE_DYNAMIC;
	}
	// <<설명>>
	/*1. BindFlags : 이 버퍼가 GPU에서 어떤 용도로 바인딩될 지 설명하기 위함, D3D11_BIND_VERTEX_BUFFER는 버텍스 버퍼로 사용하겠다는 뜻*/ 
	/*2. ByteWidth : 버퍼의 크기(byte 단위), (Vertex 크기 * Vertex 갯수)로 정해줌*/
	/*3. CPUAccessFlags : CPU가 버퍼에 접근할 수 있는지 설정*/
	/*4. Usage : 버퍼의 사용 방식을 정의함, D3D11_USAGE_DEFAULT는 GPU가 사용하는 버퍼로 설정하여 CPU접근 불가 설정, D3D11_USAGE_DYNAMIC은 CPU가 자주 수정하는 버퍼*/

	// D3D11_SUBRESOURCE_DATA : 버퍼 또는 리소스를 생성할 때 초기 데이터를 GPU에 전달하기 위한 구조체
	D3D11_SUBRESOURCE_DATA Data;
	Data.pSysMem = _Data;
	// pSysMem은 리소스에 복사할 원본 데이터의 포인터
	// 두 개 더 있는데, 버퍼 생성시에는 하지않음

	// VertexBuffer 생성
	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateBuffer(&VertexBufferInfo, &Data, VertexBuffer.GetAddressOf()))
	{
		MsgAssert("버텍스 버퍼 생성에 실패했습니다.");
	}
	// <<설명>>
	/*1. D3D11_BUFFER_DESC 전달*/
	/*2. D3D11_SUBRESOURCE_DATA 전달*/
	/*3. ID3D11Buffer 전달*/
}

 

리소스를 만들 곳에서 CreateVertexBuffer()를 호출하면, 리소스 매니저 컨테이너에 데이터를 저장하고 D3D11_BUFFER_DESC 구조체 정보를 정의하여 정점 버퍼를 어떻게 활용할 것인지 정해줍니다.

BindFlags - 현재 생성하려는 Buffer가 어떤 것인지 정의
- 여기서는 Vertex Buffer를 만드는 것이기 떄문에 D3D11_BIND_VERTEX_BUFFER를 입력
ByteWidth - 정점의 크기와 갯수
- 위에서 InputLayout에 POSITION, COLOR, TEXCOORD, NORAML 네 개를 정의했기 때문에 시멘틱 하나 당 16byte, 4개면 64byte가 됨
- 나중에 정점이 몇개 들어오는지에 따라 달라지지만, 삼각형 기준으로는 정점이 3개이기 때문에 192byte
CPUAccessFlags - CPU가 버퍼에 접근할 수 있는지에 대해 설정
Usage - 버퍼의 사용 방식 정의
- D3D11_USAGE_DEFAULT는 GPU가 사용하는 버퍼로 설정하여 CPU가 접근 못하도록 함
- DYNAMIC은 CPU가 자주 수정하는 버퍼로 설정

 

이후 CreateBuffer() 함수를 호출하여 Vertex Buffer를 생성합니다.

 

 

[Index Buffer]

 

Index Buffer는 정점을 그리는 순서에 대한 정보를 담는 버퍼입니다.

////////////////////////////////// Ext_DirectXIndexBuffer.h
#pragma once
#include "Ext_ResourceManager.h"

// 인덱스 버퍼(Index Buffer) 생성을 위한 클래스
class Ext_DirectXIndexBuffer : public Ext_ResourceManager<Ext_DirectXIndexBuffer>
{
	friend class Ext_DirectXMesh;

public:
	// constrcuter destructer
	Ext_DirectXIndexBuffer() {}
	~Ext_DirectXIndexBuffer() {}

	// delete Function
	Ext_DirectXIndexBuffer(const Ext_DirectXIndexBuffer& _Other) = delete;
	Ext_DirectXIndexBuffer(Ext_DirectXIndexBuffer&& _Other) noexcept = delete;
	Ext_DirectXIndexBuffer& operator=(const Ext_DirectXIndexBuffer& _Other) = delete;
	Ext_DirectXIndexBuffer& operator=(Ext_DirectXIndexBuffer&& _Other) noexcept = delete;

	// IndexBuffer 생성
	template<typename Type>
	static std::shared_ptr<Ext_DirectXIndexBuffer> CreateIndexBuffer(std::string_view _Name, const std::vector<Type>& _Vertexs)
	{
		std::shared_ptr<Ext_DirectXIndexBuffer> NewIndexBuffer = Ext_ResourceManager::CreateNameResource(_Name);
		NewIndexBuffer->CreateIndexBuffer(&_Vertexs[0], sizeof(Type), static_cast<UINT>(_Vertexs.size()));

		return NewIndexBuffer;
	}

	COMPTR<ID3D11Buffer>& GetIndexBuffer() { return IndexBuffer; }
	UINT GetVertexSize() { return VertexSize; }
	UINT GetVertexCount() { return VertexCount; }
	UINT GetBufferSize() { return IndexBufferInfo.ByteWidth; }

protected:
	
private:
	void CreateIndexBuffer(const void* _Data, UINT _IndexSize, UINT _IndexCount); // IndexBuffer 생성
	
	D3D11_BUFFER_DESC IndexBufferInfo = { 0, };  // 인덱스 버퍼 DESC 저장용
	COMPTR<ID3D11Buffer> IndexBuffer = nullptr;   // 인덱스 버퍼 인터페이스 저장용
	UINT VertexSize = 0;											// 버텍스 사이즈
	UINT VertexCount = 0;										// 버텍스 갯수
	DXGI_FORMAT Format = DXGI_FORMAT::DXGI_FORMAT_R32_UINT;

	UINT Offset = 0;
};

////////////////////////////////// Ext_DirectXIndexBuffer.cpp
#include "PrecompileHeader.h"
#include "Ext_DirectXIndexBuffer.h"
#include "Ext_DirectXDevice.h"

// IndexBuffer 생성
void Ext_DirectXIndexBuffer::CreateIndexBuffer(const void* _Data, UINT _IndexSize, UINT _IndexCount)
{
	VertexSize = _IndexSize;

	switch (VertexSize)
	{
	case 2:
		Format = DXGI_FORMAT_R16_UINT;
		break;
	case 4:
		Format = DXGI_FORMAT_R32_UINT;
		break;
	default:
		break;
	}

	VertexCount = _IndexCount;

	IndexBufferInfo.BindFlags = D3D11_BIND_INDEX_BUFFER;
	IndexBufferInfo.ByteWidth = VertexSize * VertexCount;
	IndexBufferInfo.CPUAccessFlags = 0;
	if (0 == IndexBufferInfo.CPUAccessFlags)
	{
		IndexBufferInfo.Usage = D3D11_USAGE_DEFAULT;
	}
	else 
	{
		IndexBufferInfo.Usage = D3D11_USAGE_DYNAMIC;
	}

	D3D11_SUBRESOURCE_DATA Data;
	Data.pSysMem = _Data;

	if (S_OK != Ext_DirectXDevice::GetDevice()->CreateBuffer(&IndexBufferInfo, &Data, IndexBuffer.GetAddressOf()))
	{
		MsgAssert("버텍스 버퍼 생성에 실패했습니다.");
	}
}

 

마찬가지로 버퍼이기 때문에 VertexBuffer와 생성 방식이 유사합니다. 차이는 BindFlags가 INDEX_BUFFER인 것 밖에 없습니다.

 

 

[버퍼 생성하기]

 

InputLayout으로 정점 정보를 정의했고, VertexBuffer, IndexBuffer를 생성하는 클래스를 만들었으니 아래와 같이 호출하면 버퍼를 생성할 수 있습니다.

// 삼각형
std::vector<Ext_DirectXVertexData> Vertices;
Vertices.resize(3);

Vertices[0] = { { 0.0f, 0.5f, 0.0 }, { 1, 0, 0, 1 } };
Vertices[1] = { { 0.5f, -0.5f, 0.0f }, { 0, 1, 0, 1 } };
Vertices[2] = { { -0.5f, -0.5f, 0.0f }, { 0, 0, 1, 1 } };

std::vector<UINT> ArrIndex = { 0, 1, 2 };

Ext_DirectXVertexBuffer::CreateVertexBuffer("Triangle", Vertices);
Ext_DirectXIndexBuffer::CreateIndexBuffer("Triangle", ArrIndex);

//,,,

// 사각형
std::vector<Ext_DirectXVertexData> Vertices;
Vertices.resize(4);

ArrVertex[0] = { { 0.5f,  0.5f, -0.5f, 1.0f}, {1, 0, 0, 1}, {1, 0}, {0, 0, -1} };
ArrVertex[1] = { {-0.5f,  0.5f, -0.5f, 1.0f}, {0, 1, 0, 1}, {0, 0}, {0, 0, -1} };
ArrVertex[2] = { {-0.5f, -0.5f, -0.5f, 1.0f}, {0, 0, 1, 1}, {0, 1}, {0, 0, -1} };
ArrVertex[3] = { { 0.5f, -0.5f, -0.5f, 1.0f}, {1, 1, 0, 1}, {1, 1}, {0, 0, -1} };

std::vector<UINT> ArrIndex = { 0, 1, 2, 0, 2, 3, };

Ext_DirectXVertexBuffer::CreateVertexBuffer("Rect", Vertices);
Ext_DirectXIndexBuffer::CreateIndexBuffer("Rect", ArrIndex);

 

IndexBuffer에 대해, 삼각형은 { 0, 1, 2 }을 전달하고 있습니다. 이러면 3개의 정점에 대해 그리는 순서를 정의해주는 것인데, 그리는 순서가 0 -> 1 -> 2가 됩니다.

 

그러면 사각형은 { 0, 1, 2, 3 }으로 설정하여 0 -> 1 -> 2 -> 3이 될 것 같지만, 아닙니다.

 

Direct3D에서 면으로 그려질 수 있는 가장 작은 단위는 삼각형이고, 렌더링 과정에서는 정점들을 최대한 재사용할 수 있도록 설계되어 있습니다. 따라서 사각형도 나눠보면 삼각형 두 개로 그릴 수 있기 때문에, 삼각형 두 개를 그리기 위한 정보를 전달해줘야 합니다. 여기서는 { 0, 1, 2, 0, 2, 3 }를 전달해주고 있습니다. 이러면 왼쪽 위에 하나, 오른쪽 아래에 하나로 총 두개의 삼각형이 그려져서 사각형을 이루게 됩니다.

 

 

[셰이더 컴파일]

 

이제 정점 정보들을 GPU가 어떻게 처리할 것인지를 정의해줘야 합니다. 이를 위해 Shader가 필요한데, 자세한 설명은 나중에 자세히 하는 것으로 하고 일단 셰이더를 만들어보겠습니다. 아래와 같이 Vertex Shader, Pixel Shader를 만들어줍니다.

///////////// BaseVertexShader.hlsl
struct VSInput
{
    float3 Position : POSITION;
    float4 Color : COLOR;
};

struct PSInput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
};

PSInput main(VSInput _Input)
{
    PSInput Output;
    Output.Position = float4(_Input.Position, 1.0f);
    Output.Color = _Input.Color;
    return Output;
}

///////////// BasePixelShader.hlsl
struct PSInput
{
    float4 Position : SV_POSITION;
    float4 Color : COLOR;
};

float4 main(PSInput _Input) : SV_TARGET
{
    return _Input.Color;
}

 

struct 내에는 시멘틱 정보들인 POSITION, COLOR, TEXCOORD, NORMAL을 모두 안써도 되고, "순서대로"만 쓰면 됩니다. 현재 사용할 정보는 POSITION과 COLOR이기 때문에 2개만 작성해줍니다. Vertex Shader와 Pixel Shader의 엔트리 포인트 모두 그냥 값을 그대로 받아서 다시 그대로 return하는 임시적인 형태로 만들었습니다.

 

다음으로 셰이더를 컴파일해줍니다. 컴파일은 아래와 같이 실시해주면 됩니다.

void Ext_DirectXResourceLoader::ShaderCompile() 
{
	unsigned int Flag = 0;

#ifdef _DEBUG
	Flag = D3D10_SHADER_DEBUG; // 디버그 정보를 포함
#endif
	Flag |= D3DCOMPILE_PACK_MATRIX_ROW_MAJOR; // 행우선 매트릭스 정렬 방식 사용 (HLSL ↔ C++ 호환 용이)

	// ID3DBlob : 셰이더 바이트코드를 담는 인터페이스
	COMPTR<ID3DBlob> ErrorBlob = nullptr;
	COMPTR<ID3DBlob> VSBlob = nullptr;
	if (S_OK != D3DCompileFromFile(L"../Shader/BaseVertexShader.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "vs_5_0", Flag, 0, VSBlob.GetAddressOf(), ErrorBlob.GetAddressOf()))
	{
		MsgAssert("VertexShader 컴파일 실패");
		return;
	}
	// <<설명>>
	/*1. 파일 경로 입력*/
	/*2. 기본 include 처리 방식 설정*/
	/*3. 함수명*/
	/*4. 셰이더 모델, 비주얼 스튜디오 2022에서 기본 생성하면 Vertex Shader Model 5.0이기 떄문에 vs_5_0*/
	/*5. 컴파일 플레그 설정*/
	/*6. 컴파일 플레그 설정*/
	/*7. 출력 바이트코드*/
	/*8. 출력 에러*/

	// 컴파일된 바이트코드로 GPU용 Vertex Shader 생성
	Ext_DirectXDevice::GetDevice()->CreateVertexShader(VSBlob->GetBufferPointer(), VSBlob->GetBufferSize(), nullptr, &BaseVertexShader);

	COMPTR<ID3DBlob> PSBlob = nullptr;
	if (S_OK != D3DCompileFromFile(L"../Shader/BasePixelShader.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "ps_5_0", Flag, 0, PSBlob.GetAddressOf(), ErrorBlob.GetAddressOf()))
	{
		MsgAssert("PixelShader 컴파일 실패");
		return;
	}

	// 컴파일된 바이트코드로 GPU용 Pixel Shader 생성
	Ext_DirectXDevice::GetDevice()->CreatePixelShader(PSBlob->GetBufferPointer(), PSBlob->GetBufferSize(), nullptr, &BasePixelShader);

	// CreateInputLayout은 정점 버퍼 구조와 셰이더 입력 구조 간의 매핑을 정의
	// 일단 생성해야되서 적음, 구조 잡으면서 나중에 옮길 예정
	Ext_DirectXDevice::GetDevice()->CreateInputLayout
	(
		Ext_DirectXInputLayout::GetInputLayoutData().GetInputLayoutDescs().data(),
		static_cast<UINT>(Ext_DirectXInputLayout::GetInputLayoutData().GetInputLayoutDescs().size()),
		VSBlob->GetBufferPointer(),
		VSBlob->GetBufferSize(),
		&InputLayout
	);

	// 각 결과물은 일단 이곳의 클래스에서 저장했다. 다른곳에서 써야함
	// 구조 잡으면서 나중에 옮길 예정
	// static COMPTR<ID3D11VertexShader> BaseVertexShader;
	// static COMPTR<ID3D11PixelShader> BasePixelShader;
	// static COMPTR<ID3D11InputLayout> InputLayout;
}

 

여기서 Path를 넣어주는 부분에 상대 경로를 넣어줘도 되지만, Unicode 형태로 넣어줘야 합니다. 안그러면 경로가 제대로 읽히지 않아 셰이더 컴파일이 실패하게 됩니다.

 

 

[CreateInputLayout]

 

정보들을 정의하고 값을 담아준 뒤 어떻게 그릴지, GPU가 어떻게 쓸 지를 모두 정해줬으니 이걸로 InputLayout을 실제로 만들어줘야합니다. 아래의 함수를 호출해줍니다.

void Ext_DirectXInputLayout::CreateInputLayout(std::shared_ptr<Ext_DirectXVertexBuffer> _VertexBuffer, std::shared_ptr< Ext_DirectXVertexShader> _VertexShader)
{
	Relase(); // 한번 해줘야 CreateInputLayout가 정상 동작함

	if (nullptr == _VertexBuffer->GetInputLayout())
	{
		MsgAssert("레이아웃 정보를 만들수 없는 버텍스 버퍼 입니다.");
	}

	const std::vector<D3D11_INPUT_ELEMENT_DESC>& LayOutInfo = _VertexBuffer->GetInputLayout()->GetInputLayoutDescs();

	// 정점 버퍼의 각 요소(Vertex Data)를 정점 셰이더의 입력 파라미터와 매핑할 수 있도록 연결하는 구조를 만드는 함수
	HRESULT hr = Ext_DirectXDevice::GetDevice()->CreateInputLayout
	(
		&LayOutInfo[0],
		static_cast<UINT>(LayOutInfo.size()),
		_VertexShader->GetBinaryCode()->GetBufferPointer(),
		_VertexShader->GetBinaryCode()->GetBufferSize(),
		InputLayout.GetAddressOf()
	);
	// <<설명>>
	/*1. const D3D11_INPUT_ELEMENT_DESC* : 정점 입력 요소 배열의 시작 주소, 각 요소가 어떤 데이터인지 정의*/
	/*2. UINT : 위 배열의 요소 수(입력 요소가 몇개인가)*/
	/*3. const void* : 정점 셰이더의 바이트 코드 시작 주소, 여기에는 입력 시그니처가 포함되어 있어야 함*/
	/*4. SIZE_T : 위 바이트코드의 길이*/
	/*5. ID3D11InputLayout** : 호출의 결과로 생성될 InputLayout 객체의 포인터 주소 저장->IASetInputLayout에서 활용*/

	if (S_OK != hr)
	{
		char Buffer[256] = {};
		sprintf_s(Buffer, "CreateInputLayout failed: 0x%08X", hr);
		MsgAssert(Buffer)
		return;
	}
}

 

CreateInputLayout() 함수에 인자들을 전달하여 InputLayout을 만들 수 있는데, 각 인자들의 정보는 다음과 같습니다.

const D3D11_INPUT_ELEMENT_DESC* - 정점 입력 요소 정보를 담은 배열의 시작 주소
- 전역 컨테이너인 InputLayOutDesc에 정보들을 담아줬기 때문에, 이걸 넣어주면 됨
UINT - InputLayOutDesc의 요소 갯수
const void* - Vertex Shader의 ByteCode 시작 주소
SIZE_T - Vertex Shader의 ByteCode 길이
ID3D11InputLayout** - 생성 결과물을 담을 포인터 전달

 

+) CreateInputLayout() 호출 과정에서 값을 받아볼 InputLayout의 경우, 명시적으로 한 번 Reset을 실시해주는 것이 좋을 수도 있습니다. 일전에 계속 생성에 실패해서 그냥 Reset을 한 뒤 넣으니 정상적으로 생성됐습니다. 아마 초기화가 제대로 되지 않아 쓰레기값이 담겨 있어서 그런 것으로 생각됩니다.

 

 

[결과 출력]

 

여기까지 진행했으면 바로 삼각형, 사각형을 출력해볼 수 있습니다. 아래와 같이 작성해줍니다.

void Ext_Core::RenderTest()
{
	// 1. 메인 렌더 타겟 가져오기
	std::shared_ptr<Ext_DirectXRenderTarget> MainRenderTarget = Ext_DirectXDevice::GetMainRenderTarget();
	COMPTR<ID3D11RenderTargetView> RTV = MainRenderTarget->GetTexture(0)->GetRTV();
	COMPTR<ID3D11DepthStencilView> DSV = MainRenderTarget->GetDepthTexture()->GetDSV();
	D3D11_VIEWPORT* ViewPort = MainRenderTarget->GetViewPort(0);

	// 2. 렌더 타겟 및 뎁스 클리어
	float ClearColor[4] = { 0.0f, 0.0f, 1.0f, 1.0f }; // 파란색
	Ext_DirectXDevice::GetContext()->ClearRenderTargetView(RTV.Get(), ClearColor);
	Ext_DirectXDevice::GetContext()->ClearDepthStencilView(DSV.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    
	// 3. 렌더 타겟 세팅하기
	Ext_DirectXDevice::GetContext()->OMSetRenderTargets(1, RTV.GetAddressOf(), DSV.Get());
	Ext_DirectXDevice::GetContext()->RSSetViewports(1, ViewPort);

	// 4. ==========렌더링==============
	std::shared_ptr<Ext_DirectXVertexBuffer> VB = Ext_DirectXVertexBuffer::Find("Triangle");
	std::shared_ptr<Ext_DirectXIndexBuffer> IB = Ext_DirectXIndexBuffer::Find("Triangle");
	COMPTR<ID3D11Buffer>& VertexBuffer = VB->GetVertexBuffer();
	UINT stride = VB->GetVertexSize();
	UINT Offset = 0;

	Ext_DirectXDevice::GetContext()->IASetVertexBuffers(0, 1, VertexBuffer.GetAddressOf(), &stride, &Offset);
	Ext_DirectXDevice::GetContext()->IASetIndexBuffer(IB->GetIndexBuffer().Get(), DXGI_FORMAT_R32_UINT, 0);

	Ext_DirectXDevice::GetContext()->IASetInputLayout(Ext_DirectXResourceLoader::GetInputLayout().Get());
	Ext_DirectXDevice::GetContext()->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	Ext_DirectXDevice::GetContext()->VSSetShader(Ext_DirectXResourceLoader::GetVertexShader(), nullptr, 0);
	Ext_DirectXDevice::GetContext()->PSSetShader(Ext_DirectXResourceLoader::GetPixelShader(), nullptr, 0);

	Ext_DirectXDevice::GetContext()->DrawIndexed(IB->GetVertexCount(), 0, 0);
	// ==========렌더링 끝==============

	// 5. 화면 출력
	Ext_DirectXDevice::GetSwapChain()->Present(1, 0);
}

 

1 ~ 3. 기존과 동일함

 

4-1. 렌더링 시작 준비

: Triangle 정보가 담긴 Vertex Buffer와 Index Buffer를 가져옵니다.

 

4-2. Vertex Buffer와 Index Buffer 바인딩

: IASetVertexBuffer()와 IASetIndexBuffer() 함수를 통해 렌더링 파이프라인에 두 버퍼를 바인딩해줍니다. IASetVertexBuffer()의 경우 전달 인자 값이 다음과 같습니다.

> 1번 인자 : Vertex Buffer 슬롯, 해당 프레임워크에서는 하나만 사용하기 때문에 그냥 0으로 넣어줌

> 2번 인자 : Vertex Buffer 갯수, 하나만 사용

> 3번 인자 : Vertex Buffer 주소(CreateBuffer()할 때 반환받은 값)

> 4번 인자 : 정점 하나당 크기, stride라고 함

> 5번 인자 : 버퍼 시작 오프셋, 보통 0임

 

IASetIndexBuffer()는 전달 인자 값이 다음과 같습니다.

> 1번 인자 : Index Buffer 주소(CreateBuffer()할때 반환받은 값)

> 2번 인자 : Index Buffer 데이터 타입, 보통 DXGI_FORMAT_R32_UINT 전달하면 됨(UINT)

> 3번 인자 : 버퍼 시작 오프셋, 보통 0임

 

4-3. InputLayout 바인딩

: IASetInputLayout() 함수를 호출하여 정점의 정보를 바인딩해줍니다. CreateInputLayout()을 통해 반환받은 값을 넣어줍니다.

 

4-4. 출력 단위 설정

: IASetPrimitiveTopology() 함수를 호출하여 출력 단위를 설정합니다. 기본적으로는 삼각형을 사용하기 때문에, 정점들을 삼각형으로 해석할 수 있도록 D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST를 전달해줍니다(라인, 포인트 등도 존재함).

 

4-5. 셰이더 바인딩

: VSSetShader() 함수와 PSSetShader() 함수를 통해 셰이더를 바인딩해줍니다. 앞서 CreateVertexShader()와 CreatePixelShader()를 통해 반환받은 포인터를 넣고 나머지 인자로 nullptr, 0을 전달합니다. 셰이더에 대해 따로 사용할 클래스가 있으면 전달하지만, 해당 프레임워크에서는 따로 만들어서 사용하지 않을 예정이니 이렇게 전달해주면 됩니다.

 

4-6. DrawIndexed 호출

: 위에서 렌더링 파이프라인에 필요한 정보들을 바인딩 했으니, DrawIndexed()를 호출하면 결과물이 화면에 출력됩니다.

 

5-1. 삼각형 결과 확인

 

5-2. 시각형 결과 확인

: Vertex Buffer와 Index Buffer를 Rect로 전달해주면 사각형으로 그려집니다.

 

 

[예상치 못한 결과들]

 

출력 결과가 이상하게 보일 것입니다. 우리의 목표는 정삼각형, 정사각형이었지만 화면에서는 삼각형과 직사각형이 그러지기 때문입니다. 이것은 아직 뷰, 프로젝션 행렬을 적용하지 않았기 때문에 좌표 변환이 정상적으로 이뤄지지 않았기 때문입니다. 

 

현재 화면 비율은 16:9 비율로 ViewPort가 설정되어 있습니다. Vertex Buffer에 정의한 값이 그대로 NDC(Normalized Device Coordinates) 좌표에 그려지지만, ViewPort 변환이 수행되기 떄문에 위의 출력 결과물들 처럼 옆으로 늘어진 형태가 되는 것입니다. 해당 문제에 대해서는 이후 포스팅에서 자세히 다루면서 문제를 해결해보도록 하겠습니다.

 

또한 정점 별로 색을 지정해줬는데, 결과물은 보간된 값이 나옵니다. 이건 어떻게 보면 정상적인 출력 결과인데, Rasterizer 단계에서 픽셀 단위로 보간이 수행되기 때문입니다. 픽셀이 정점들과 얼마나 가까운지 계산되면서 Color값이 자동으로 선형 보간되기 때문에 결과와 같이 혼합된 색상이 나오는 것입니다.

[투 포인터]

 

고정된 크기의 배열에 대해, 배열에 접근할 수 있는 포인터 두 가지(Left, Right)를 활용하여 탐색하는 방법이다. 보통 정렬된 상태의 연속 수열의 부분 합을 구할 때 많이 활용한다.

int TwoPointer(vector<int> _Arr, int _Value)
{
    int Result = 0;
    int Left = 0, Right = 0;
    int Sum = _Arr[0];
    int Size = _Arr.size();

    vector<int> answer;

    while (Left < Size && Right < Size)
    {
        if (Sum < _Value)
        {
            ++Right;
            if (Right < Size)
                Sum += _Arr[Right];
        }
        else if (Sum > _Value)
        {
            Sum -= _Arr[Left];
            ++Left;
        }
        else // Sum == _Value
        {
            if (/*특정 동작을 위한 if문*/)
            {
                // TODO
                ++Result;
            }

            // Sum == _Value인 경우에도 줄여서 다음 가능성 탐색
            // 없으면 그냥 break;
            Sum -= _Arr[Left];
            ++Left;
        }
    }

    return Result;
}

 

 

[슬라이딩 윈도우]

 

두 개의 포인터가 유동적으로 변하는 투 포인터와 달리, 슬라이딩 윈도우는 고정된 크기의 공간에 대한 탐색을 실시한다. 

int SlidingWindow(vector<int> _Arr, int _Value, int _WindowSize)
{
    int Result = 0;
    int Sum = 0;

    // 초기 윈도우 계산
    for (int i = 0; i < _WindowSize; ++i)
    {
        Sum += _Arr[i];
    }

    if (Sum == _Value)
    {
        ++Result;
    }

    // 오른쪽으로 윈도우 이동
    for (int i = _WindowSize; i < _Arr.size(); ++i)
    {
        Sum -= _Arr[i - _WindowSize]; // 왼쪽 끝 제거
        Sum += _Arr[i];                      // 오른쪽 새 값 추가

        if (Sum == _Value)
        {
            ++Result;
        }
    }

    return Result;
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/178870

 

[풀이]

 

투 포인터를 활용하면 쉽게 풀 수 있다. 0번 인덱스부터 시작하여, 현재의 sum이 k보다 작으면 Right를 옮기면서 sum에 합해주고, sum이 k보다 크면 Left를 옮기면서 sum에서 빼준다.

 

같은 경우를 찾아도 계속 탐색을 진행해야하기 때문에 이때는 두 인덱스간 차를 먼저 검사하여 저장된 인덱스 차보다 현재 Right, Left의 차가 더 작으면 answer를 갱신해준다. 이후 Left를 한 칸 오른쪽으로 옮기면서 sum에서 Left 옮긴 값 만큼 빼주고 계속 탐색을 진행하는 것이다.

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> solution(vector<int> sequence, int k)
{
    int Left = 0, Right = 0;
    int sum = sequence[0];
    int n = sequence.size();

    int minLength = INT_MAX;
    vector<int> answer;

    while (Left < n && Right < n)
    {
        if (sum < k)
        {
            ++Right;
            if (Right < n)
                sum += sequence[Right];
        }
        else if (sum > k)
        {
            sum -= sequence[Left];
            ++Left;
        }
        else // sum == k
        {
            if ((Right - Left) < minLength)
            {
                minLength = Right - Left;
                answer = { Left, Right };
            }

            // sum == k인 경우에도 줄여서 다음 가능성 탐색
            sum -= sequence[Left];
            ++Left;
        }
    }

    return answer;
}

[문제]

https://school.programmers.co.kr/learn/courses/30/lessons/68645

 

[풀이]

처음에는 규칙성을 찾고 인덱스를 하나씩 선형 탐색하면서 push_back을 하려다가(좀 오래 고민함), 이건 아닌거 같아서 다른 방법을 생각했다. 먼저 2차원 배열을 준비한 다음 아래, 오른쪽, 위-왼쪽(대각선) 방향으로 채워나가는 방식이다.

 

1. 2차원 테이블 생성

2. 각 인덱스 값에 접근할 요소들 선언

3. 탐색 시작, max값이 되면 종료한다.

  3-1. 방문한 테이블 좌표값에 대해 num을 입력하고, 1 증가시켜준다.

  3-2. 현재 방향에 대해 먼저 y를 1 증가, x는 그대로 탐색 시도, 이 값이 경계를 벗어났는지 확인함

    3-2-1. 경계를 벗어난 경우(사이즈를 벗어나거나 값이 채워진 인덱스인 경우), 방향 전환을 시도

    3-2-2. 시도 방식은 dir(방향 인덱스)를 재설정하고, 해당 방향 인덱스를 기준으로 탐색할 좌표값을 덮어써줌

  3-3. 경계가 벗어나지 않았으면 좌표를 재설정

4. 3의 방식을 반복하여 값을 채워넣은 뒤, 2차원 배열을 answer에 담고 return

 

규칙성을 찾지 않고 그냥 채워넣었으면 금방 풀었을 것이다(아쉬운 부분). 아래는 구현 코드이다.

#include <vector>

using namespace std;

int dy[3] = { 1, 0, -1 };
int dx[3] = { 0, 1, -1 };

vector<int> solution(int n)
{
    // 삼각형 구조를 표현할 2차원 벡터
    vector<vector<int>> triangle(n, vector<int>(n, 0));

    // 방향 벡터: 아래, 오른쪽, 왼쪽 위 대각선
    int dy[3] = { 1, 0, -1 };
    int dx[3] = { 0, 1, -1 };

    int y = 0;
    int x = 0;
    int num = 1;
    int dir = 0; // 방향 인덱스
    int maxNum = n * (n + 1) / 2;

    while (num <= maxNum) 
    {
        triangle[y][x] = num++;

        int ny = y + dy[dir];
        int nx = x + dx[dir];

        // 경계를 벗어나거나 이미 숫자가 채워져 있으면 방향 전환
        if (ny >= n || nx >= n || ny < 0 || nx < 0 || triangle[ny][nx] != 0) 
        {
            dir = (dir + 1) % 3;
            ny = y + dy[dir];
            nx = x + dx[dir];
        }

        y = ny;
        x = nx;
    }

    // 결과를 1차원 벡터로 변환
    vector<int> answer;
    for (int i = 0; i < n; i++) 
    {
        for (int j = 0; j <= i; j++) 
        {
            answer.push_back(triangle[i][j]);
        }
    }

    return answer;
}

+ Recent posts