서의 공간

4. Buffers, Shaders, and HLSL 본문

Graphics API/DirectX 11 - Rastertek

4. Buffers, Shaders, and HLSL

홍서의 2021. 1. 7. 06:58

이번 튜토리얼에서는 DirectX 11에서 정점 및 픽셀 셰이더를 작성하는 방법에 대해 알아볼 것이다. 또한 DirectX 11에서 정점 및 인덱스 버퍼를 사용하는 방법을 소개한다. 이는 3D 그래픽을 렌더링 하기 위해 이해하고 활용해야 하는 가장 기본적인 개념이다.

 

Vertex Buffers

첫 번째 개념은 정점 버퍼이다. 이 개념을 설명하기 위해 다음 구의 3D 모델을 예로 들어 살펴본다.

우리가 화면에서 바라보는 구의 이미지는 실제로 다음과 같이 수백 개의 삼각형으로 구성된다.

구 모델의 각 삼각형은 3개의 점으로 이루어 있고 이 점을 정점(꼭짓점)이라고 한다. 이러한 구 모델을 렌더링 하려면 구를 형성하는 모든 정점을 정점 버퍼라고 하는 특수한 데이터 배열에 넣어야 한다. 구 모델의 모든 점을 정점 버퍼에 넣고 GPU로 보내게 되면 주어진 점의 데이터에 맞추어 모델을 렌더링 할 수 있게 된다.

 

Index Buffers

인덱스 버퍼는 정점 버퍼와 관련된 버퍼이다. 인덱스 버퍼의 목적은 정점 버퍼에 있는 각 정점의 위치를 기록하는 것이다. 그런 다음 GPU는 인덱스 버퍼를 사용하여 정점 버퍼에서의 특정 정점을 빠르게 찾는다. 인덱스 버퍼의 개념은 사전(dictionary)의 색인을 사용하는 개념과 유사하며, 사전에서처럼 원하는 주제를 훨씬 더 빠른 속도로 찾는데 도움이 된다. DirectX 문서에 따르면 인덱스 버퍼를 사용하면 비디오 메모리의 더 빠른 위치에 정점 데이터를 캐싱할 가능성이 높아질 수 있다. 따라서 성능상의 이유로 사용하는 것이 좋다.

 

Vertex Shaders

정점 셰이더는 주로 정점 버퍼의 정점을 3D 공간으로 변환하기 위해 작성된 작은 프로그램이다. 각 정점에 대한 노멀 계산과 같이 수행할 수 있는 계산이 있는데, 정점 셰이더 프로그램은 각 정점에 대해 GPU에 의해 병렬적으로 호출되어 처리한다. 예를 들어 5,000개의 삼각형을 그리기 위해 한 프레임 당 15,000번 정점 셰이더 프로그램을 실행한다(삼각형의 정점이 3개이므로). 따라서 만약 fps를 60으로 고정하면 정점 셰이더를 초당 900,000번 호출하여 5,000개의 삼각형을 그리게 된다. 이렇듯이 정점 셰이더를 효율적이게 작성하는 것은 매우 중요하다.

 

Pixel Shaders

픽셀 셰이더는 화면에 그려질 다각형의 색상을 지정하기 위해 작성된 작은 프로그램이다. 화면에 그려질 모든 가시 픽셀의 색상이 GPU에 의해 수행된다. 다각형 면에 적용하려는 색상, 텍스처링, 조명, 및 기타 대부분의 효과는 픽셀 셰이더 프로그램에서 처리한다. 픽셀 셰이더는 GPU에서 호출되는 횟수 때문에 효율적으로 작성해야 한다.

 

HLSL

HLSL은 정점 및 픽셀 셰이더 프로그램을 코딩하기 위해 DirectX 11에서 사용하는 언어이다. 문법은 C언어와 거의 동일하다. HLSL 프로그램 파일은 전역 변수, 구조체 정의, 정점 셰이더, 픽셀 셰이더 및 지오메트리 셰이더로 구성된다. 

 

프레임워크

프레임워크가 업데이트되었다. GraphicsClass 아래에 CameraClass, ModelClass 및 ColorShaderClass라는 세 가지 새로운 클래스가 추가되었다. CameraClass는 이전에 언급한 뷰 행렬을 처리한다. 월드에서 카메라의 위치를 처리하고 우리가 씬의 어느 위치에서 어느 곳을 보고 있는지 파악해야 할 때 뷰 행렬을 계산하여 셰이더에 전달한다. ModelClass는 3D 모델의 지오메트리(기하구조)를 처리한다. 이번 튜토리얼에서는 간단하게 하나의 삼각형만 살펴본다. 마지막으로 ColorShaderClass는 HLSL 셰이더를 적용하여 화면에 모델을 렌더링 하는 클래스이다.

먼저 HLSL 셰이더 프로그램을 살펴보는 것으로 튜토리얼을 시작한다.


Color.vs

첫 번째 셰이더 프로그램을 작성해보자. 셰이더는 모델의 실제 렌더링을 수행하는 작은 프로그램이다. 이러한 셰이더는 HLSL로 작성하여 Color.vs 및 Color.ps라는 소스 파일에 저장된다. Visual Studio에서 솔루션 탐색기를 통해 파일을 만든 후 마우스 오른쪽 버튼을 클릭하여 속성으로 들어가 항목 형식이 "빌드에 참여 안 함"으로 설정해야 한다. 이렇게 설정하지 않으면 컴파일 오류가 발생한다.

이 셰이더의 목적은 색깔이 있는 삼각형을 그리는 것이다. 다음은 정점 셰이더의 코드이다.

cbuffer MatrixBuffer
{
	matrix worldMatrix;
	matrix viewMatrix;
	matrix projectionMatrix;
};

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

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

PixelInputType ColorVertexShader(VertexInputType input)
{
	PixelInputType output;
	
	// 적절한 행렬 계산을 위해 위치 벡터를 동차 좌표로 변환한다.
	input.position.w = 1.0f;

	// 월드, 뷰, 프로젝션 행렬들을 이용해 정점의 위치를 계산한다.
	output.position = mul(input.position, worldMatrix);
	output.position = mul(output.position, viewMatrix);
	output.position = mul(output.position, projectionMatrix);

	// 입력받은 색상을 그대로 픽셀 셰이더에서 이용하도록 저장한다.
	output.color = input.color;

	return output;
}

이 셰이더 프로그램인 전역 변수로 시작한다. 여기서 전역 변수는 C++코드에서 외부적으로 수정할 수 있다. C++에서 int 또는 float과 같은 다양한 유형의 변수를 사용한 다음 셰이더 프로그램에서 사용할 수 있도록 값을 넘겨줄 수 있다는 것이다. 하나의 기본 타입의 전역 변수라 할지라도 항상 cbuffer라는 버퍼 구조체에 전역 변수를 넣는다. 이렇게 하는 이유는 셰이더의 효율적인 실행과 그래픽 카드가 버퍼를 저장하는 방법이 있기 때문에 매우 중요하다. 이 예제에서는 3개의 행렬을 매 프레임 같은 시간에 업데이트할 수 있도록 동일한 cbuffer 구조체에 넣었다.

HLSL에서는 C언어와 유사하게 새로운 구조체 타입을 정의할 수 있다. 또한 프로그래밍 셰이더를 읽기 쉽게 만들도록 float4와 같은 다양한 데이터 타입을 사용한다. 이 예제에서는 (x, y, z, w) 위치 벡터와 빨강, 초록, 파랑, 알파 색상을 가진 데이터 타입을 정의한다. POSITION, COLOR 및 SV_POSITION은 변수 사용을 GPU에 전달하는 시맨틱이다. 정점 셰이더에서 사용할 구조체와 픽셀 셰이더에서 사용할 구조체의 구조가 동일하더라도 정점 셰이더와 픽셀 셰이더에서의 각 변수의 의미가 달라지기 때문에 두 개의 서로 다른 구조체를 만들어야 한다. POSITION은 정점 셰이더에서 작동하고 SV_POSITION은 픽셀 셰이더에서 작동하고 COLOR는 두 곳 모두에서 작동한다. 동일한 데이터 타입을 두 개 이상 원할 경우 끝에 COLOR0, COLOR1 등과 같이 숫자를 추가하면 된다.

정점 셰이더는 전송된 정점 버퍼의 데이터를 처리할 때 GPU에 의해 호출된다. ColorVertexShader() 함수는 정점 버퍼에 있는 모든 각 정점에 대해 호출된다. 정점 셰이더의 입력은 정점 버퍼의 데이터 타입과 셰이더 소스 파일의 데이터 타입(여기서는 VertexInputType)과 일치해야 한다. 정점 셰이더의 출력은 픽셀 셰이더로 전송된다. 이 경우 출력 데이터 타입은 이미 정의한 PixelInputType이다.

ColorVertexShader()를 살펴보면 정점 셰이더가 출력하기 위해 PixelInputType타입의 변수를 생성하는 것을 볼 수 있다.

그런 다음 파라미터로 주어진 VertexInputType타입의 input 구조체 변수를 통해 위치(position)를 가져온다. 위치를 가져와서 월드, 뷰, 프로젝션 행렬을 곱한다. 그러면 뷰에 따라 3D 공간에서 렌더링 한 다음 2D 화면에 렌더링 할 올바른 정점의 위치가 결정된다. 그 후 output변수의 color 속성은 input의 color를 그대로 복사하여 픽셀 셰이더의 입력으로 사용한다. 또한 input의 위치 속성의 w값을 1.0f로 설정했는데, 1.0f로 설정하지 않으면 정의되지 않는다.

 

Color.ps

픽셀 셰이더는 화면에 렌더링 될 다각형에 각 픽셀을 그린다. 이 픽셀 셰이더에서는 PixelInputType을 입력으로 사용하고 최종 픽셀 색상을 float4타입으로 반환한다. 이 픽셀 셰이더 프로그램은 간단하게 색상의 입력 값과 동일하게 픽셀에 색상을 지정하도록 한다. 픽셀 셰이더는 정점 셰이더 출력에서 입력을 받는다.

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

float4 ColorPixelShader(PixelInputType input) : SV_TARGET
{
	return input.color;
}

ColorShaderClass.h

ColorShaderClass는 GPU에 있는 3D 모델을 그리기 위해 HLSL 셰이더를 호출하는데 사용한다.

MatrixBufferType 구조체는 정점 셰이더에 있는 cbuffer 타입의 정의다. 이 구조체의 타입은 셰이더의 구조체와 정확히 일치해야 한다.

Initialize(), Shutdown() 함수는 셰이더의 초기화 및 종료를 수행한다. Render() 함수는 셰이더에서 사용할 파라미터를 설정하고 준비된 모델의 정점을 그린다.

#ifndef _COLORSHADERCLASS_H_
#define _COLORSHADERCLASS_H_

// INCLUDES
#include <d3d11.h>
#include <d3dcompiler.h>
#include <DirectXMath.h>
#include <fstream>
using namespace DirectX;
using namespace std;

class ColorShaderClass
{
private:
	struct MatrixBufferType
	{
		XMMATRIX world;
		XMMATRIX view;
		XMMATRIX projection;
	};

public:
	ColorShaderClass();
	ColorShaderClass(const ColorShaderClass&);
	~ColorShaderClass();

	bool Initialize(ID3D11Device*, HWND);
	void Shutdown();
	bool Render(ID3D11DeviceContext*, int, XMMATRIX, XMMATRIX, XMMATRIX);

private:
	bool InitializeShader(ID3D11Device*, HWND, const WCHAR*, const WCHAR*);
	void ShutdownShader();
	void OutputShaderErrorMessage(ID3DBlob*, HWND, const WCHAR*);

	bool SetShaderParameters(ID3D11DeviceContext*, XMMATRIX, XMMATRIX, XMMATRIX);
	void RenderShader(ID3D11DeviceContext*, int);

private:
	ID3D11VertexShader* mVertexShader;
	ID3D11PixelShader* mPixelShader;
	ID3D11InputLayout* mLayout;
	ID3D11Buffer* mMatrixBuffer;
};
#endif

 

ColorShaderClass.cpp

Initialize() 함수는 InitializeShader() 함수를 호출하여 셰이더를 초기화한다. HLSL 셰이더 파일의 이름을 파라미터로 전달한다.

Render() 함수는 먼저 SetShaderParameters() 함수를 호출하여 셰이더 내부의 파라미터를 설정한다. 파라미터가 설정되면 RenderShader() 함수를 호출하여 HLSL 셰이더를 사용해 초록색 삼각형을 그린다.

 

InitializeShader() 함수는 셰이더 파일을 로드하여 DirectX 및 GPU에서 사용할 수 있도록 한다. 또한 레이아웃 설정과 GPU의 그래픽 파이프라인에서 정점 버퍼 데이터가 어떻게 보이는지 볼 수 있다. 레이아웃은 ModelClass.h 파일의 VertexType과 Color.vs 파일에 정의된 VertexInputType과 일치해야 한다.

셰이더를 컴파일 하기 위해 D3DCompileFromFile() 함수를 호출한다. 파라미터로는 셰이더 파일 이름, 셰이더 이름(셰이더 내부에 정의한 셰이더 함수 이름), 셰이더 버전 및 셰이더를 컴파일하여 결과를 저장할 버퍼를 지정한다. 셰이더 컴파일에 실패하면 오류를 작성하기 위해 errorMessage버퍼에 오류 메시지를 넣는다. 만약 errorMessage에 오류 메시지가 없는데 컴파일에 실패한다면 셰이더 파일을 찾을 수 없다는 의미이므로, 오류 대화 상자를 팝업 한다. ID3DBlob 참고 할 것.

정점 셰이더와 픽셀 셰이더의 컴파일이 성공하여 버퍼에 저장되었다면 device를 통해 CreateVertexShader()및 CreatePixelShader() 함수를 호출하여 각 셰이더의 객체를 만든다.

다음 단계는 셰이더에서 처리할 정점 데이터의 레이아웃을 만드는 것이다. 셰이더에서 정점의 위치와 색상 데이터를 하므로, 이 두 종류의 데이터의 크기를 지정하여 D3D11_INPUT_ELEMENT_DESC을 작성한다. 시맨틱 이름은 실제로 셰이더로 넘겨질 변수와 매치되어야 한다. 레이아웃에서 또 중요한 부분은 정점 데이터 타입이다. 위치 벡터에는 DXGI_FORMAT_R32G32B32_FLOAT을 사용하고 색상에는 DXGI_FORMAT_R32G32B32A32_FLOAT을 사용한다. 마지막으로 주의해야 할 것은 버퍼에 있는 데이터가 얼마 큼의 간격을 두고 있는지를 나타내는 AlignedByteOffset이다. 이 레이아웃의 경우 처음 12바이트는 위치이고(float타입이 3개이므로), 다음 16바이트는 색상이 된다. 색상의 레이아웃의 AlignedByteOffset에 자신의 값을 배치하는 대신 D3D11_APPEND_ALIGNED_ELEMENT를 사용할 수 있다. 위치는 버퍼의 시작 부분이므로 0으로 설정하면 된다.

레이아웃의 description을 모두 작성했다면 데이터 구조체의 요소가 몇 개 인지 확인하고 device를 이용해 레이아웃을 생성한다. 또한 레이아웃이 생성되면 정점 셰이더 버퍼와 픽셀 셰이더 버퍼는 더 이상 사용하지 않으므로 해제한다. 

 

OutputShaderErrorMessage()는 정점 셰이더 또는 픽셀 셰이더를 컴파일할 때 오류가 발생하면 오류 메시지를 작성하는 함수이다.

 

SetShaderVariables() 함수는 셰이더에서 전역 변수를 보다 쉽게 설정하기 위한 함수이다. 파라미터인 3개의 행렬은 GraphicsClass, CameraClass에서 만들어진 후 넘어온다. 그럼 해당 함수는 파라미터로 받은 행렬들을 정점 셰이더에 전송한다.

행렬들을 정점 셰이더에 보내기 전에 각 행렬들의 전치하여 전치된 행렬을 넘기는데, 이것은 DirectX 11의 요구사항이다.

다음 단계에서는 Map() 함수를 호출하여 셰이더로 넘겨질 상수 버퍼인 mMatrixBuffer를 잠그고 방금 계산한 새로운 행렬들을 넣어주고 잠금을 해제한다.

마지막으로 새로운 행렬 값들로 업데이트된 행렬 버퍼를 정점 셰이더에 설정한다.

 

RenderShader() 함수는 Render() 함수에서 호출하는 두 번째 함수이다. 

이 함수의 첫 번째 단계는 Input Assembler단계에서 위에서 만든 레이아웃을 바인딩한다. 이를 통해 GPU는 정점 버퍼의 데이터 형식을 알 수 있게 된다. 두 번째 단계는 정점 버퍼를 렌더링 하는 데 사용할 정점 셰이더와 픽셀 셰이더를 바인딩한다. 마지막으로 DrawIndexed() 함수를 호출하여 삼각형을 렌더링 한다.

#include "ColorShaderClass.h"

ColorShaderClass::ColorShaderClass()
{
	mVertexShader = 0;
	mPixelShader = 0;
	mLayout = 0;
	mMatrixBuffer = 0;
}

ColorShaderClass::ColorShaderClass(const ColorShaderClass & other)
{
}

ColorShaderClass::~ColorShaderClass()
{
}

bool ColorShaderClass::Initialize(ID3D11Device * device, HWND hwnd)
{
	bool result;

	// Initialize the vertex and pixel shaders.
	result = InitializeShader(device, hwnd, L"../Rastertek/Color.vs", L"../Rastertek/Color.ps");
	if (!result) return false;

	return true;
}

void ColorShaderClass::Shutdown()
{
	// Shutdown the vertex and pixel shaders as well as the related objects.
	ShutdownShader();

	return;
}

bool ColorShaderClass::Render(ID3D11DeviceContext * deviceContext, int indexCount,
	XMMATRIX worldMatrix, XMMATRIX viewMatrix, XMMATRIX projectionMatrix)
{
	bool result;

	// Set the shader parameters that it will use for rendering.
	result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix);
	if (!result) return false;
	// Now render the prepared buffers with the shader.
	RenderShader(deviceContext, indexCount);

	return true;
}

bool ColorShaderClass::InitializeShader(ID3D11Device * device, HWND hwnd, 
	const WCHAR * vsFileName, const WCHAR * psFileName)
{
	HRESULT result;
	ID3DBlob* errorMessage;
	ID3DBlob* vertexShaderBuffer;
	ID3DBlob* pixelShaderBuffer;
	D3D11_INPUT_ELEMENT_DESC polygonLayout[2];
	unsigned int numElements;
	D3D11_BUFFER_DESC matrixBufferDesc;

	// Initialize the pointers this function will use to null.
	errorMessage = 0;
	vertexShaderBuffer = 0;
	pixelShaderBuffer = 0;

	// Compile the vertex shader code.
	result = D3DCompileFromFile(vsFileName, nullptr, nullptr, "ColorVertexShader", "vs_5_0", 
		D3D10_SHADER_ENABLE_STRICTNESS, 0, &vertexShaderBuffer, &errorMessage);
	if (FAILED(result))
	{
		// If the shader failed to compile it should have writen something to the error message.
		if (errorMessage)
		{
			OutputShaderErrorMessage(errorMessage, hwnd, vsFileName);
		}
		// If there was nothing in the error message then it simply could not fine the shader file itself.
		else
		{
			MessageBox(hwnd, vsFileName, L"Missing Shader File", MB_OK);
		}

		return false;
	}

	// Compile the pixel shader code.
	result = D3DCompileFromFile(psFileName, nullptr, nullptr, "ColorPixelShader", "ps_5_0", 
		D3D10_SHADER_ENABLE_STRICTNESS, 0, &pixelShaderBuffer, &errorMessage);
	if (FAILED(result))
	{
		// If the shader failed to compile it should have writen something to the error message.
		if (errorMessage)
		{
			OutputShaderErrorMessage(errorMessage, hwnd, psFileName);
		}
		// If there was nothing in the error message then it simply could not find the file itself.
		else
		{
			MessageBox(hwnd, psFileName, L"Missing Shader File", MB_OK);
		}

		return false;
	}

	// Create the vertex shader from the buffer.
	// ID3D11VertexShader 객체 생성
	result = device->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), 
		vertexShaderBuffer->GetBufferSize(), nullptr, &mVertexShader);
	if (FAILED(result)) return false;

	// Create the pixel shader from the buffer.
	// ID3D11PixelShade 객체 생성
	result = device->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), 
		pixelShaderBuffer->GetBufferSize(), nullptr, &mPixelShader);
	if (FAILED(result)) return false;

	// Create the vertex input layout description.
	// This setup needs to match the VertexType structure in the ModelClass and in the shader.
	polygonLayout[0].SemanticName = "POSITION";
	polygonLayout[0].SemanticIndex = 0;
	polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[0].InputSlot = 0;
	polygonLayout[0].AlignedByteOffset = 0;
	polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[0].InstanceDataStepRate = 0;

	polygonLayout[1].SemanticName = "Color";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;

	// Get a count of the elements in the layout.
	numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

	// Create the vertex input layout.
	result = device->CreateInputLayout(polygonLayout, numElements, 
		vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &mLayout);
	if (FAILED(result)) return false;

	// Release the vertex shader buffer and pixel shader buffer since they are no longer needed.
	// ID3D11VertextShader, ID3D11PixelShader객체에 bytecode가 저장되어 있음. 더이상 버퍼는 필요없다.
	vertexShaderBuffer->Release();
	vertexShaderBuffer = 0;

	pixelShaderBuffer->Release();
	pixelShaderBuffer = 0;

	// Setup the description of the dynamic matrix constant buffer that is in the vertex shader.
	matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
	matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType);
	matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
	matrixBufferDesc.MiscFlags = 0;
	matrixBufferDesc.StructureByteStride = 0;

	// Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
	result = device->CreateBuffer(&matrixBufferDesc, nullptr, &mMatrixBuffer);
	if (FAILED(result)) return false;

	return true;
}

void ColorShaderClass::ShutdownShader()
{
	// Release the matrix constant buffer.
	if (mMatrixBuffer)
	{
		mMatrixBuffer->Release();
		mMatrixBuffer = 0;
	}
	// Release the layout.
	if (mLayout)
	{
		mLayout->Release();
		mLayout = 0;
	}
	// Release the pixel shader.
	if (mPixelShader)
	{
		mPixelShader->Release();
		mPixelShader = 0;
	}
	// Release the vertex shader.
	if (mVertexShader)
	{
		mVertexShader->Release();
		mVertexShader = 0;
	}

	return;
}

void ColorShaderClass::OutputShaderErrorMessage(ID3DBlob * errorMessage, HWND hwnd, const WCHAR * shaderFileName)
{
	char* compileErrors;
	unsigned long long bufferSize, i;
	ofstream fout;

	// Get a pointer to the error message text buffer.
	compileErrors = (char*)(errorMessage->GetBufferPointer());

	// Get the length of the message.
	bufferSize = errorMessage->GetBufferSize();

	// Open a file to write the error message to.
	fout.open("shader-error.txt");

	// Write out the error message.
	for (i = 0; i < bufferSize; i++)
	{
		fout << compileErrors[i];
	}

	// Close the file.
	fout.close();

	// Release the error message.
	errorMessage->Release();
	errorMessage = 0;

	// Pop a message up on the screen to notify the user to check the text file for compile errors.
	MessageBox(hwnd, L"Error compiling shader. Check shader-error.txt for message.",
		shaderFileName, MB_OK);

	return;
}

bool ColorShaderClass::SetShaderParameters(ID3D11DeviceContext * deviceContext, 
	XMMATRIX worldMatrix, XMMATRIX viewMatrix, XMMATRIX projectionMatrix)
{
	HRESULT result;
	D3D11_MAPPED_SUBRESOURCE mappedResource;
	MatrixBufferType* dataPtr;
	unsigned int bufferNumber;

	// Transpose the matrices to prepare them for the shader.
	worldMatrix = XMMatrixTranspose(worldMatrix);
	viewMatrix = XMMatrixTranspose(viewMatrix);
	projectionMatrix = XMMatrixTranspose(projectionMatrix);

	// Lock the constant buffer so it can be written to.
	result = deviceContext->Map(mMatrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
	if (FAILED(result)) return false;
	// Get a pointer to the data in the constant buffer.
	// MatrixBufferType은 클래스에서 정의한 변환 매트릭스 구조체이다.
	// 여기서 알 수 있는 것은 D3D11_MAPPED_SUBRESOURCE에 수정한 데이터를 입력하는 곳이고,
	// 여기를 통해서 mMatrixBuffer를 수정한다. mMatrixBuffer는 변환을 위한 constant버퍼임
	dataPtr = (MatrixBufferType*)mappedResource.pData;
	// Copy the matrices into the constant buffer.
	dataPtr->world = worldMatrix;
	dataPtr->view = viewMatrix;
	dataPtr->projection = projectionMatrix;
	// Unlock the constant buffer.
	deviceContext->Unmap(mMatrixBuffer, 0);

	// Set the position of the constant buffer in the vertex shader.
	bufferNumber = 0;

	// Finaly set the constant buffer in the vertex shader with the updated values.
	deviceContext->VSSetConstantBuffers(bufferNumber, 1, &mMatrixBuffer);

	return true;
}

void ColorShaderClass::RenderShader(ID3D11DeviceContext * deviceContext, int indexCount)
{
	// Set the vertex input layout.
	deviceContext->IASetInputLayout(mLayout);

	// Set the vertex and pixel shaders that will be used to render this triangle.
	deviceContext->VSSetShader(mVertexShader, nullptr, 0);
	deviceContext->PSSetShader(mPixelShader, nullptr, 0);

	// Render the triangle.
	deviceContext->DrawIndexed(indexCount, 0, 0);

	return;
}

ModelClass.h

앞서 언급했듯이 ModelClass는 3D 모델의 지오메트리를 캡슐화한다. 이 튜토리얼에서는 초록색 삼각형의 데이터를 직접 수동으로 설정한다. 또한 삼각형을 렌더링 할 수 있도록 정점 및 인덱스 버퍼를 생성한다.

클래스에서 3D 모델을 그리기 위해 정점 버퍼에 넣을 정점의 타입을 VertexType 구조체로 선언한다. 또한 이 VertexType타입은 뒷부분에서 보게 될 ColorShaderClass의 레이아웃과 일치해야 한다.

Initailize()와 Shutdown()는 모델의 정점 버퍼 및 인덱스 버퍼의 초기화 및 종료를 처리하고, Render() 함수는 모델 지오메트리를 비디오 카드에 전달하여 셰이더로 그릴 준비를 한다.

4개의 private 변수 중에서 ID3D11Buffer 타입의 두 변수는 일반적으로 DIrectX 11의 버퍼를 생성할 때 사용된다. 이 클래스에서 정점 버퍼와 인덱스 버퍼를 만들어야 하므로 선언했다. 나머지 int 타입의 두 변수는 각각 정점의 개수와 인덱스의 개수를 추적하기 위한 변수이다.

#ifndef _MODELCLASS_H_
#define _MODELCLASS_H_

// INCLUDES
#include <d3d11.h>
#include <DirectXMath.h>
using namespace DirectX;

class ModelClass
{
private:
	struct VertexType
	{
		XMFLOAT3 position;
		XMFLOAT4 color;
	};

public:
	ModelClass();
	ModelClass(const ModelClass&);
	~ModelClass();
	bool Initialize(ID3D11Device*);
	void Shutdown();
	void Render(ID3D11DeviceContext*);
	int GetIndexCount();

private:
	bool InitializeBuffers(ID3D11Device*);
	void ShutdownBuffers();
	void RenderBuffers(ID3D11DeviceContext*);

private:
	ID3D11Buffer* mVertexBuffer;
	ID3D11Buffer* mIndexBuffer;
	int mVertexCount, mIndexCount;
};
#endif

 

ModelClass.cpp

Initialize() 함수에서는 InitializeBuffers() 함수를 호출하여 정점 버퍼와 인덱스 버퍼를 초기화한다.

Shutdown() 함수에서는 ShutdownBuffers() 함수를 호출하여 정점 버퍼와 인덱스 버퍼를 종료한다.

Render() 함수에서는 RenderBuffers() 함수를 호출하여 셰이더가 렌더링 할 수 있도록 정점 버퍼와 인덱스 버퍼를 그래픽 파이프라인에 올린다.

 

InitializeBuffers() 함수는 정점 버퍼와 인덱스 버퍼 생성을 수행한다. 일반적으로 모델의 데이터 파일을 읽고 그 데이터를 사용하여 버퍼를 만든다. 현재 이 튜토리얼에서는 하나의 삼각형만 그리기 때문에 정점 및 인덱스를 수동을 설정한다.

처음에는 먼저 임시로 정점과 인덱스 데이터를 저장할 두 개의 배열을 만든다. 각각 VertexType의 배열과 unsigned long의 배열이다. 이 두 배열은 마지막에 버퍼를 채우기 위해 사용된다. 

두 배열을 만들었으면 이제 데이터를 채운다. indices에서 정점의 인덱스를 설정할 때, 정점은 항상 시계 방향으로 그려야 한다. 시계 반대 방향으로 정점을 배치하면 삼각형이 반대 방향을 향하고 생각하고 컬링으로 인해 그리지 않게 된다. 정점을 GPU로 보내는 순서가 매우 중요하다는 것을 항상 기억하자. 각 정점의 색상은 모두 초록색으로 설정했다.

정점 배열과 인덱스 배열이 채웠다면 이를 사용하여 정점 버퍼와 인덱스 버퍼를 만들 수 있다. 하지만 또 그전에 버퍼의 description을 작성해야 한다. description에서 Usage, ByteWidth, BindFlags는 매우 중요한 속성이므로 D3D11_BUFFER_DESC를 참고한다. description을 작성했다면 D3D11_SUBRESOURCE_DATA도 작성해야 한다. 그 후 device(ID3D11Device* 타입인)를 사용하여 CreateBuffer를 호출하여 버퍼를 생성한다. 가장 마지막 파라미터는 생성한 버퍼의 포인터를 저장할 출력 파라미터이다. 정점 버퍼와 인덱스 버퍼를 만들었다면 임시로 만든 정점 배열과 인덱스 배열은 더 이상 사용하지 않으므로 해제한다.

 

RenderBuffers() 함수는 GPU의 Input Assembler단계에서 정점 버퍼와 인덱스 버퍼를 바인딩하는 것이다. GPU에 바인딩된 정점 버퍼가 있으면 셰이더를 사용하여 해당 버퍼를 렌더링 할 수 있다. 또한 이 함수는 삼각형, 선 등과 같이 버퍼를 그리는 방법을 정의한다. D3D11_PRIMITIVE_TOPOLOGY를 참고한다.

#include "ModelClass.h"

ModelClass::ModelClass()
{
	mVertexBuffer = 0;
	mIndexBuffer = 0;
}

ModelClass::ModelClass(const ModelClass &)
{
}

ModelClass::~ModelClass()
{
}

bool ModelClass::Initialize(ID3D11Device * device)
{
	bool result;

	// Initialize the vertex and index buffers.
	result = InitializeBuffers(device);
	if (!result) return false;

	return true;
}

void ModelClass::Shutdown()
{
	// Shutdown the vertex and index buffers.
	ShutdownBuffers();

	return;
}

void ModelClass::Render(ID3D11DeviceContext * deviceContext)
{
	// Put the vertex and index buffers on the graphics pipeline to prepare them for drawing.
	RenderBuffers(deviceContext);

	return;
}

int ModelClass::GetIndexCount()
{
	return mIndexCount;
}

bool ModelClass::InitializeBuffers(ID3D11Device * device)
{
	VertexType* vertices;
	unsigned long* indices;
	D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
	D3D11_SUBRESOURCE_DATA vertexData, indexData;
	HRESULT result;

	// Set the number of vertices in the vertex array.
	mVertexCount = 3;
	// Set the number of indices in the index array.
	mIndexCount = 3;
	// Create the vertex array.
	vertices = new VertexType[mVertexCount];
	if (!vertices) return false;
	// Create the index array.
	indices = new unsigned long[mIndexCount];
	if (!indices) return false;

	// Load the vertex array with data.
	vertices[0].position = XMFLOAT3(-1.0f, -1.0f, 0.0f);  // Bottom left.
	vertices[0].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[1].position = XMFLOAT3(0.0f, 1.0f, 0.0f);  // Top middle.
	vertices[1].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	vertices[2].position = XMFLOAT3(1.0f, -1.0f, 0.0f);  // Bottom right.
	vertices[2].color = XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f);

	// Load the index array with data.
	indices[0] = 0;  // Bottom left.
	indices[1] = 1;  // Top middle.
	indices[2] = 2;  // Bottom right.

	// Set up the description of the static vertex buffer.
	vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
	vertexBufferDesc.ByteWidth = sizeof(VertexType) * mVertexCount;
	vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vertexBufferDesc.CPUAccessFlags = 0;
	vertexBufferDesc.MiscFlags = 0;
	vertexBufferDesc.StructureByteStride = 0;
	// Give the subresource structure a pointer to the vertex data.
	vertexData.pSysMem = vertices;
	vertexData.SysMemPitch = 0;
	vertexData.SysMemSlicePitch = 0;
	// Now create the vertex buffer.
	result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &mVertexBuffer);
	if (FAILED(result)) return false;

	// Set up the description of the static index buffer.
	indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
	indexBufferDesc.ByteWidth = sizeof(unsigned long) * mIndexCount;
	indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
	indexBufferDesc.CPUAccessFlags = 0;
	indexBufferDesc.MiscFlags = 0;
	indexBufferDesc.StructureByteStride = 0;
	// Give the subresource structure a pointer to the index data.
	indexData.pSysMem = indices;
	indexData.SysMemPitch = 0;
	indexData.SysMemSlicePitch = 0;
	// Create the index buffer.
	result = device->CreateBuffer(&indexBufferDesc, &indexData, &mIndexBuffer);
	if (FAILED(result)) return false;

	// Release the arrays now that the vertex and index buffers have been created and loaded.
	delete[] vertices;
	vertices = 0;
	delete[] indices;
	indices = 0;

	return true;
}

void ModelClass::ShutdownBuffers()
{
	// Release the index buffer.
	if (mIndexBuffer)
	{
		mIndexBuffer->Release();
		mIndexBuffer = 0;
	}
	// Release the vertex buffer.
	if (mVertexBuffer)
	{
		mVertexBuffer->Release();
		mVertexBuffer = 0;
	}

	return;
}

void ModelClass::RenderBuffers(ID3D11DeviceContext * deviceContext)
{
	unsigned int stride;
	unsigned int offset;

	// Set vertex buffer stride and offset.
	stride = sizeof(VertexType);
	offset = 0;
	// Set the vertex buffer to active in the input assembler so it can be rendered.
	deviceContext->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &offset);
	// Set the index buffer to active in the input assembler so it can be rendered.
	deviceContext->IASetIndexBuffer(mIndexBuffer, DXGI_FORMAT_R32_UINT, 0);
	// Set the type of primitive that should be rendered from this vertex buffer, in this case triangles.
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	return;
}

CameraClass.h

카메라 클래스는 카메라의 위치와 현재 회전을 추적한다. 위치 및 회전 정보를 사용하여 HLSL 셰이더로 전달되는 뷰 행렬을 생성한다. 뷰 행렬을 렌더링을 위한 행렬 중 하나이다.

 

SetPosition() 및 SetRotation() 함수는 카메라 객체의 위치와 회전을 설정하는 데 사용된다. Render() 함수는 카메라의 위치와 회전을 기반으로 뷰 행렬을 만드는 데 사용된다. 마지막으로 GetViewMatrix는 셰이더가 렌더링에 사용할 수 있도록 카메라 객체에서 뷰 행렬을 구하는 데 사용된다.

#ifndef _CAMERACLASS_H_
#define _CAMERACLASS_H_

// INCLUDES
#include <DirectXMath.h>
using namespace DirectX;

class CameraClass
{
public:
	CameraClass();
	CameraClass(const CameraClass&);
	~CameraClass();

	void SetPosition(float, float, float);
	void SetRotation(float, float, float);
	XMFLOAT3 GetPosition();
	XMFLOAT3 GetRotation();
	void Render();
	void GetViewMatrix(XMMATRIX&);

private:
	float mPositionX, mPositionY, mPositionZ;
	float mRotationX, mRotationY, mRotationZ;
	XMMATRIX mViewMatrix;
};
#endif

 

CameraClass.cpp

Render() 함수는 카메라의 위치와 회전을 사용하여 뷰 행렬을 만들고 업데이트한다. 뷰 행렬을 만들기 위해서 카메라의 (월드 기준으로) 업 벡터, 위치, 회전 변수 등을 설정한다. lookAt 변수는 카메라가 바라보는 방향벡터로서 기본 +z 방향으로 초기화하고, 카메라를 x, y, z 회전을 기준으로 회전한다. 그런 다음 회전된 카메라를 3D 공간의 위치로 변환한다. 위치, lookAt, 업 벡터를 파라미터로 XMMatrixLookAtLH() 함수를 호출하여 카메라 공간으로의 변환을 나타내는 뷰 행렬을 만들 수 있게 된다.

#include "CameraClass.h"

CameraClass::CameraClass()
{
	mPositionX = 0.0f;
	mPositionY = 0.0f;
	mPositionZ = 0.0f;
	mRotationX = 0.0f;
	mRotationY = 0.0f;
	mRotationZ = 0.0f;
}

CameraClass::CameraClass(const CameraClass &)
{
}

CameraClass::~CameraClass()
{
}

void CameraClass::SetPosition(float x, float y, float z) 
{
	mPositionX = x;
	mPositionY = y;
	mPositionZ = z;
	return;
}

void CameraClass::SetRotation(float x, float y, float z)
{
	mRotationX = x;
	mRotationY = y;
	mRotationZ = z;
	return;
}

XMFLOAT3 CameraClass::GetPosition()
{
	return { mPositionX, mPositionY, mPositionZ };
}

XMFLOAT3 CameraClass::GetRotation()
{
	return { mRotationX, mRotationY, mRotationZ };
}

void CameraClass::Render()
{
	XMFLOAT3 up, position, lookAt;
	XMVECTOR upVector, positionVector, lookAtVector;
	float yaw, pitch, roll;
	XMMATRIX rotationMatrix;

	// Setup the vector that points upward.
	up.x = 0.0f;
	up.y = 1.0f;
	up.z = 0.0f;
	// Load it into a XMVECTOR structure.
	upVector = XMLoadFloat3(&up);
	// Setup the position of the camera in the world.
	position.x = mPositionX;
	position.y = mPositionY;
	position.z = mPositionZ;
	// Load it into a XMVECTOR structure.
	positionVector = XMLoadFloat3(&position);
	// Setup where the camera is looking by default.
	lookAt.x = 0.0f;
	lookAt.y = 0.0f;
	lookAt.z = 1.0f;
	// Load it into a XMVECTOR structure.
	lookAtVector = XMLoadFloat3(&lookAt);
	// Set the yaw (Y axis), pitch (X axis), roll (Z axis) rotations in radians.
	pitch = mRotationX * 0.0174532925f;
	yaw = mRotationY * 0.0174532925f;
	roll = mRotationZ * 0.0174532925f;
	// Create the rotation matrix from the yaw, pitch, and roll values.
	rotationMatrix = XMMatrixRotationRollPitchYaw(pitch, yaw, roll);
	// Transform the lookAt and up vector by the rotation matrix so the view is correctly rotated at the origin.
	lookAtVector = XMVector3TransformCoord(lookAtVector, rotationMatrix);
	// Transform the rotated camera position to the location of the viewer.
	/* 여기서 positionVector를 더하는 이유는
	lookAtVector에 이동변환을 적용한 것이다. 이동변환은 더하기. */
	lookAtVector = XMVectorAdd(positionVector, lookAtVector);
	// Finally create the view matrix from the three updated vectors.
	mViewMatrix = XMMatrixLookAtLH(positionVector, lookAtVector, upVector);

	/*
	XMMatrixLookAtLH()는 카메라의 위치, 바라보는 방향벡터, 업벡터를 파라미터로 요구
	카메라의 위치는 월드 위치이고 SetPosition()에서 셋팅함. 기본은 (0, 0, 0)에 위치
	바라보는 위치는 카메라가 회전할 수도 있으니까 회전한 각을 적용해주어야 한다.
	0.0174532925는 파이/180임, 도에서 라디안으로 바꾸기 위해서다.
	*/
	return;
}

void CameraClass::GetViewMatrix(XMMATRIX & viewMatrix)
{
	viewMatrix = mViewMatrix;
	return;
}

실행화면

'Graphics API > DirectX 11 - Rastertek' 카테고리의 다른 글

6. Diffuse Lighting  (0) 2021.01.07
5. Texturing  (0) 2021.01.07
3. Initializing DirectX 11  (0) 2021.01.07
2. Creating a Framework and Window  (0) 2021.01.07
1. Setting up DirectX 11 with Visual Studio  (0) 2021.01.07
Comments