DirectX11

DirectX11 학습 - 삼각형 띄우기

영춘권의달인 2024. 3. 26. 16:27

 

가상의 공간에 등장하는 3D 모델들은 사실 삼각형으로 이루어져있다. 수많은 삼각형들로 기하학적인 모형을 만들고 그 위에 텍스쳐를 입혀서 화면에 출력하는 것이 우리가 보는 3D모델의 모습이다.

 

가장 기본이 되는 삼각형을 화면에 띄워볼건데, 삼각형을 띄우는 작업도 처음에는 생각보다 간단하지 않다.

 

struct VertexColorData
{
	Vec3 position = { 0, 0, 0 };
	Color color = { 0, 0, 0, 0 };
};

vector<VertexColorData> _vertices;
vector<uint32> _indices;

void Triangle::Init()
{
	_shader = make_shared<Shader>(L"_Triangle.fx");

	_vertices.resize(3);

	// 정점 정보 설정
	_vertices[0].position = Vec3(-0.5f, -0.5f, 0.f);
	_vertices[0].color = Color(1.f, 0.f, 0.f, 1.f);
	_vertices[1].position = Vec3(-0.5f, 0.5f, 0.f);
	_vertices[1].color = Color(0.f, 1.f, 0.f, 1.f);
	_vertices[2].position = Vec3(0.5f, -0.5f, 0.f);
	_vertices[2].color = Color(0.f, 0.f, 1.f, 1.f);

	// 정점 정보를 통해 정점 버퍼 생성
	_vertexBuffer = make_shared<VertexBuffer>();
	_vertexBuffer->Create(_vertices);

	_indices.resize(3);
	// 인덱스 정보 설정
	_indices[0] = 0;
	_indices[1] = 1;
	_indices[2] = 2;

	// 인덱스 정보를 통해 인덱스 버퍼 생성
	_indexBuffer = make_shared<IndexBuffer>();
	_indexBuffer->Create(_indices);
}

NDC 좌표계

 

우선 사용할 쉐이더를 로드한다. 이부분은 조금 뒤에 다루도록 하겠다.

삼각형은 정점이 3개이기 때문에 3개의 정점을 만들고, 각 정점에는 Position과 Color값을 넣어주도록 하겠다.

정점에 넣어주는 값은 쉐이더에서 무슨 작업을 할지에 따라 다르게 넣어줄 수 있다. 지금은 위치와 색상값만 넣어주도록 하고, 현재 카메라는 아직 등장하지 않았기 때문에 NDC좌표계를 기준으로 한 위치값을 할당해주도록 하겠다. 

생성한 정점 정보를 통해 정점 버퍼를 만들어주는데, 생성한 정보를 렌더링 파이프라인에 전달하기 위한 과정이라고 생각하면 된다.

 

다음은 인덱스 정보를 만들어준다. 왼쪽아래, 왼쪽위, 오른쪽아래 순서로 0,1,2의 정보를 넣어주고 인덱스 정보를 통해 인덱스 버퍼를 만들어준다.

인덱스 정보를 사용하는 이유는 정점 정보를 중복해서 사용하는 일이 없게 하기 위함이다. 만약 사각형을 그린다고 가정했을 때, 삼각형 두개를 이어붙혀서 만들 수 있는데 이때 정점이 6개가 되고 2개의 정점이 중복해서 사용된다.

하지만 인덱스 버퍼를 사용한다면 정점 정보는 4개만 올려놓고 어떤 순서로 정점을 이어붙혀서 삼각형을 그릴지 정해줄 수 있다. 물론 이렇게 하면 인덱스값은 중복이 되지만 인덱스 정보는 정수 하나의 정보이고 정점 하나하나마다 무거운 정보가 들어갈 수 있기 때문에 인덱스 버퍼를 사용하는 것이 훨씬 효율적이다.

그리고 인덱스 정보를 설정할때는 기본적으로 삼각형이 시계방향으로 그려지도록 설정해야 한다.

이유는 최적화를 위한 Culling때문이고, RasterizerState 설정을 통해 변경할 수 있긴 하다. 이부분은 나중에 다루도록 하겠다.

 

void Triangle::Render()
{
	uint32 stride = _vertexBuffer->GetStride();
	uint32 offset = _vertexBuffer->GetOffset();

	// DeviceContext를 통해 VertexBuffer 연결
	DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
	// DeviceContext를 통해 IndexBuffer 연결
	DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);

	_shader->DrawIndexed(0, 0, _indices.size());
}

 

다음은 생성한 삼각형을 렌더하는 부분이다.

DC(DeviceContext)를 통해 VertexBuffer와 IndexBuffer를 렌더링 파이프라인 단계로 넘겨준다.

그리고 Draw를 실행시키면, 이제 직접적인 연산들은 쉐이더 코드를 통해 진행된다.

(원래는 정점 정보를 묘사해주는 InputLayout을 생성해서 넘겨줘야 하는데, technique11이라는 라이브러리를 사용하여 편리하게 쉐이더를 연결할 수 있도록 했다.)

 

쉐이더 코드로 넘어가기 전에 렌더링 파이프라인에 대한 이해가 필요하다.

렌더링 파이프라인이란 간단하게 말해서 GPU에서 렌더링 연산을 수행하는 일련의 프로세스이다.

위 단계에 알맞은 쉐이더 코드를 작성해서 넣어주면, GPU에서 해당 쉐이더 코드를 통해 연산을 해준다.

여러가지 단계가 있지만, 고급 기법들은 나중에 다루고 위의 단계 중 5개 단계만 사용하도록 하겠다.

  • Input Assembler : 모델의 기하학적인 정보를 GPU에 넘겨주는 과정이다. 위의 Render함수에서 실행한 IA~ 함수가 이 부분과 관련되어있다.
  • Vertex Shader : IA단계를 통해 전달받은 정점 정보들에 대한 연산 (3D공간 -> 2D공간 변환)
  • Rasterizer : 변환된 정점들 사이에 있는 영역들에 대한 보간, 컬링 작업
  • Pixel Shader : 모든 픽셀에 대한 연산, 빛 연산
  • Ouput Merger : 연산된 데이터 회수

이중 쉐이더에서 직접 작성할 수 있는 코드는 VertexShader, PixelShader이다.

struct VertexInput 
{
	float4 position : POSITION;
	float4 color : COLOR;
};

struct VertexOutput
{
	float4 position : SV_POSITION;
	float4 color : COLOR;
};

// Vertex Shader
VertexOutput VS(VertexInput input)
{
	VertexOutput output;
	output.position = input.position;
	output.color = input.color;
	return output;
}

// Pixel Shader
float4 PS(VertexOutput input) : SV_TARGET
{
	return input.color;
}

technique11 T0
{
	pass P0
	{
		SetVertexShader(CompileShader(vs_5_0, VS()));
		SetPixelShader(CompileShader(ps_5_0, PS()));
	}
};

 

쉐이더 문법은 c++ 문법과 크게 다르지는 않아서 코드를 작성하거나 읽는데 큰 어려움은 없었다.

VS가 Vertex Shader이고, 매개변수로 들어오는 값은 위에서 작성한 클래스에서 사용한 정점 정보와 맞춰줘야 한다. 그리고 앞에 SV_가 붙은 변수는 직접 작성한 코드뿐만 아니라 내부에서도 사용하는 값이라고 생각하면 된다.

 

애초에 최종 목적지인 NDC좌표계를 기준으로 삼각형을 그려줬기 때문에 별도의 좌표 변환 작업은 필요가 없다. 그렇기 때문에 들어온 값을 그대로 반환해준다.

 

VS 후에는 Rasterizer 과정이 실행되며, 정점 사이의 값들을 보간해준다.

 

다음은 PS단계로 들어오게 된다. 여기서 각 픽셀의 최종 색상을 정해주게 되는데, 들어온 color값을 그대로 반환해주기 때문에 정점 사이의 보간된 색상이 출력될 것을 예상할 수 있다.