[DirectX] NDC 좌표계와 쉐이더
in Study on WinAPI&DirectX
NDC 좌표계와 쉐이더를 이용해서 간단한 삼각형을 그리는 과정.
참고해서 보기 좋은 영상이 있다.
유튜브 : GPU는 어떻게 작동할까
// define.h
// 자주쓰는 매크로 설정
#define DEVICE CDevice::GetInst()->GetDevice();
#define CONTEXT CDevice::GetInst()->GetDeviceContext();
// struct.h
struct tVertex
{
Vec3 vPos; // 좌표 정보 저장
Vec4 vColor; // 색상 정보 저장
};
typedef tVertex Vtx;
//Test.h
#pragma once
void Init();
void Tick();
void Render();
void Release();
//Test.cpp
#include "pch.h" // cpp 파일은 무조건 최상단에 pch 헤더를 참조한다.
#include "Test.h"
#include "CPathMgr.h" // PathMgr 호출
#include "CDevice.h" // GPU에게 명령을 하기 위함.
// 정점 버퍼
ComPtr<ID3D11Buffer> g_VB;
// 쉐이더
ComPtr<ID3DBlob> g_VSBlob;
ComPtr<ID3DBlob> g_PSBlob;
ComPtr<ID3DBlob> g_ErrBlob; // 실패 정보를 저장하는 블롭
ComPtr<ID3D11VertexShader> g_VS; // 실제 사용할 버텍스 쉐이더
Comptr<ID3D11PixelShader> g_PS; // 실제 사용할 픽셀 쉐이더
// InputLayout
ComPtr<ID3D11InputLayout> g_Layout;
Vtx g_arrVtx[3] = {};
void Init()
{
g_arrVtx[0].vPos = Vec3(0.f, 1.f, 0.5f);
g_arrVtx[0].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
g_arrVtx[1].vPos = Vec3(1.f, -1.f, 0.5f);;
g_arrVtx[1].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
g_arrVtx[2].vPos = Vec3(-1.f, 1.f, 0.5f);;
g_arrVtx[2].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
D3D11_BUFFER_DESC tBufferDesc = {};
tBufferDesc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_VERTEX_BUFFER; // 버퍼의 용도
// SystemMemory 에서 수정 가능한 버퍼로 설정
tBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE;
tBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
// 버퍼 크기 할당
tBufferDesc.ByteWidth = sizeof(Vtx) * 3; // 삼각형을 그리기 위해 필요한 Vtx버퍼 3개
// 버퍼 생성
D3D11_SUBRESOURCE_DATA tSub = {};
tSub.pSysMem = g_arrVtx;
if(FAILED(DEVICE->CreateBuffer(&tBufferDesc, &tSub, g_VB.GetAddressOf()))
{
assert(nullptr);
}
// Shader 파일 경로
wstring strShaderFile = CPathMgr::GetInst()->GetContentPath();
strShaderFile += L"shader\\test.fx";
// VertexShader 파일 컴파일
if (FAILED(D3DCompileFromFile(strShaderFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS_Test", "vs_5_0", 0,
0, g_VSBlob.GetAddressOf(), g_ErrBlob.GetAddressOf())))
{
MessageBoxA(nullptr, (const char*)g_ErrBlob->GetBufferPointer(), "Vertex Shader Compile Failed", MB_OK);
}
// PixelShader 파일 컴파일
if (FAILED(D3DCompileFromFile(strShaderFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS_Test", "ps_5_0", 0,
0, g_PSBlob.GetAddressOf(), g_ErrBlob.GetAddressOf())))
{
MessageBoxA(nullptr, (const char*)g_ErrBlob->GetBufferPointer(), "Pixel Shader Compile Failed", MB_OK);
}
// 컴파일된 객체로 쉐이더 만들기.
DEVICE->CreateVertexShader(g_VSBlob->GetBufferPointer(), g_VSBlob->GetBufferSize(), nullptr, g_VS.GetAddressOf());
DEVICE->CreatePixelShader(g_PSBlob->GetBufferPointer(), g_PSBlob->GetBufferSize(), nullptr, g_PS.GetAddressOf());
// InputLayout 생성
D3D11_INPUT_ELEMENT_DESC LayoutDesc[2] = {};
LayoutDesc[0].SemanticName = "POSITION";
LayoutDesc[0].SemanticIndex = 0; // 이름이 중복될 경우를 대비한 작업
LayoutDesc[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; // Vec3 자료형
LayoutDesc[0].AlignedByteOffset = 0;
LayoutDesc[0].InputSlot = 0;
LayoutDesc[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[0].InstanceDataStepRate = 0;
LayoutDesc[1].SematicName = "COLOR";
LayoutDesc[1].SemanticIndex = 0;
LayoutDesc[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // Vec4 자료형
LayoutDesc[1].AlignedByteOffset = 12; // POSITION의 크기가 12바이트이므로.
LayoutDesc[1].InputSlot = 0;
LayoutDesc[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[1].InstanceDataStepRate = 0;
if(FAILED(DEVICE->CreateInputLayout(LayoutDesc, 2, g_VSBlob->GetBufferPointer(), g_VSBlob->GetBufferSize(), g_Layout.GetAddressOf())))
{
assert(nullptr);
}
}
void Tick()
{
}
void Render()
{
// IA
UINT iStride = sizeof(Vtx);
UINT iOffset = 0;
CONTEXT->IASetVertexBuffers(0, 1, g_VB.GetAddressOf(), &iStride, &iOffset);
CONTEXT->IASetInputLayout(g_Layout.Get());
CONTEXT->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST)
CONTEXT->VSSetShader(g_VS.Get(), nullptr, 0);
CONTEXT->PSSetShader(g_PS.Get(), nullptr, 0);
CONTEXT->Draw(3, 0);
}
void Release()
{
}
이렇게 작성하면 엔진에서는 cpp를 이렇게 바꾸어준다.
#include "pch.h"
#include "CEngine.h"
#include "CDevice.h"
#include "Test.h"
CEngine::CEngine()
: m_hWnd(nullptr)
{
}
CEngine::~CEngine()
{
}
int CEngine::init(HWND _hWnd, UINT _iWidth, UINT _iHeight)
{
// 메인 윈도우 핸들
m_hWnd = _hWnd;
m_vResolution = Vec2((float)_iWidth, (float)_iHeight);
// 해상도에 맞는 작업영역 크기 조정
RECT rt = { 0, 0, (int)_iWidth, (int)_iHeight};
AdjustWindowRect(&rt, WS_OVERLAPPEDWINDOW, false);
SetWindowPos(m_hWnd, nullptr, 10, 10, rt.right - rt.left, rt.bottom - rt.top, 0);
ShowWindow(m_hWnd, true);
// Device 초기화
if (FAILED(CDevice::GetInst()->init(m_hWnd, _iWidth, _iHeight)))
{
MessageBox(nullptr, L"Device 초기화 실패", L"에러", MB_OK);
return E_FAIL;
}
Init();
return S_OK;
}
void CEngine::progress()
{
tick();
render();
}
void CEngine::tick()
{
Tick();
}
void CEngine::render()
{
// 렌더 시작 및 코드
Render();
// 렌더 종료
CDevice::GetInst()->Present();
}
NDC 좌표계
위의 테스트 코드에서 아래와 같은 코드를 작성한 것을 볼 수 있는데, 이 과정에서 NDC 좌표계가 쓰였다.
Vtx arrVtx[3] = {};
arrVtx[0].vPos = Vec3(0.f, 1.f, 0.5f);
arrVtx[0].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
arrVtx[1].vPos = Vec3(1.f, -1.f, 0.5f);;
arrVtx[1].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
arrVtx[2].vPos = Vec3(-1.f, 1.f, 0.5f);;
arrVtx[2].vColor = Vec4(1.f, 1.f, 1.f, 1.f);
같은 위치에 있는 정점이라고 해도 해상도에 따라서 화면 상 정점의 위치는 달라지게 된다.
이때 이것을 보정하기 위한 좌표계가 NDC 좌표계이다.
- 좌상단 : (-1, 1)
- 우상단 : (1, 1)
- 좌 : (-1, 0)
- 우 : (1, 0)
- 좌하단 : (-1, -1)
- 우하단 : (1, 1)
- 중앙 : (0, 0)
으로 설정해서 상대적인 좌표를 계산하는 것이다.
쉐이더
삼각형을 그리는 과정은 그래픽스 파이프라인 과정을 통해서 그리게 된다.
이때 파이프라인의 각 단계를 쉐이더라고 하며 단계 순서는 아래와 같다.
- Input Assembler : 필요한 데이터(정점 버퍼 등)를 전달받는 역할
- Vertex Shader : 정점들이 각각 어디에 배치되어야 하는지 결정하는 함수
- Hull Shader
- Domain Shader
- Geometry Shader
- Rasterizer : 정점 3개로 만들어진 삼각형 범위 내에 들어오는 픽셀들을 찾는다.
- Pixel Shader : Rasterizer에서 찾은 픽셀들의 색상을 결정한다.
- Depth Stencil test
- Blending
여기서 맨 아래의 두개 (Depth Stencil test, Blending) 을 합쳐서 Output-Merge라고 한다.
또한, 쉐이더는 HLSL이라는 언어를 이용해서 작성한다.
파일 또한 따로 만들어주어야 하며, 헤더파일로 만들면서 확장자명은 .fx으로 만들어주어야한다.
그리고 #pragma once가 먹히지 않으므로 주의하자.
C++이 아닌 C 스타일로 하면서 전처리기를 활용해주어야 한다.
그렇게하면 아래와 같이 코드가 작성된다.
#ifndef _TEST
#define _TEST
// VS 입력 구조체
struct VS_IN
{
float3 vPos : POSITION; // semantic
float4 vColor : COLOR;
};
// VS 반환 구조체
struct VS_OUT
{
float4 vPosition : SV_Position;
}
// vertex shader
VS_OUT VS_Test(VS_IN _in)
{
VS_OUT output = (VS_OUT) 0.f; // HLSL 에서의 구조체 초기화 문법
output.vPosition = float4(_in.vPos, 1.f);
return output;
}
//pixel shader
float4 PS_Test(VS_OUT _in) : SV_Target // vertex shader가 반환한 값을 입력으로 받는다.
{
float4 vColor = (float4) 0.f;
vColor = float4(1.f, 0.f, 0.f, 1.f); // R G B Alpha
return vColor;
}
#endif
HLSL 확장 프로그램 (HLSL Tools for Visual Studio)를 설치해주자.
그리고 shaderCode는 기존의 CPP파일과 같이 컴파일 되는 파일이 아닌, 별도의 파일이다.
그래서 별도의 컴파일 작업을 통해서 바이너리 코드로 바꾸어야 한다.
그래서 fx파일을 실행하기 위해서는 WinAPI때 content폴더에서 리소스와 사운드를 가져왔던 것 처럼
fx 파일은 content 폴더 내에 있어야 한다.
즉, 게임의 리소스로 취급되는 것이다.
위 코드에서 작성되어있듯, HLSL을 컴파일하는 코드는 아래와 같다.
// Shader 파일 경로
wstring strShaderFile = CPathMgr::GetInst()->GetContentPath();
strShaderFile += L"shader\\test.fx";
// VertexShader 파일 컴파일
if (FAILED(D3DCompileFromFile(strShaderFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "VS_Test", "vs_5_0", 0,
0, g_VSBlob.GetAddressOf(), g_ErrBlob.GetAddressOf())))
{
MessageBoxA(nullptr, (const char*)g_ErrBlob->GetBufferPointer(), "Vertex Shader Compile Failed", MB_OK);
}
// PixelShader 파일 컴파일
if (FAILED(D3DCompileFromFile(strShaderFile.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, "PS_Test", "ps_5_0", 0,
0, g_PSBlob.GetAddressOf(), g_ErrBlob.GetAddressOf())))
{
MessageBoxA(nullptr, (const char*)g_ErrBlob->GetBufferPointer(), "Pixel Shader Compile Failed", MB_OK);
}
그리고 아래 코드를 통해 컴파일된 객체로 VertexShader, PixelShader를 만든다.
DEVICE->CreateVertexShader(g_VSBlob->GetBufferPointer(), g_VSBlob->GetBufferSize(), nullptr, g_VS.GetAddressOf());
DEVICE->CreatePixelShader(g_PSBlob->GetBufferPointer(), g_PSBlob->GetBufferSize(), nullptr, g_PS.GetAddressOf());
Layout
Layout의 역할은 무엇일까?
정확하게 Shader에서 요구하는 항목이 메모리에서 어디에 들어있는지 파악하도록 만드는 도구이다.
// VS 입력 구조체
struct VS_IN
{
float3 vPos : POSITION;
float4 vColor : COLOR;
};
구조체가 위와 같다면 메모리는 vPos, vColor 순으로 메모리를 할당할 것이다.
그리고 vPos를 먼저 읽고, vColor를 읽음으로써 정보를 불러올 것이다.
// VS 입력 구조체
struct VS_IN
{
float4 vColor : COLOR;
float3 vPos : POSITION;
};
하지만 구조체가 위와 같은 순서로 저장되어있는데 메모리를 읽는 방법이 vPos를 먼저 읽고 vColor를 읽는 순서대로 읽어버리면 정보가 꼬일 수 밖에 없다.
이를 방지하고 정확하게 컬러정보와 포지션정보를 불러오도록 하는 것이 레이아웃이다.
// InputLayout 생성
D3D11_INPUT_ELEMENT_DESC LayoutDesc[2] = {};
LayoutDesc[0].SemanticName = "POSITION";
LayoutDesc[0].SemanticIndex = 0; // 이름이 중복될 경우를 대비한 작업
LayoutDesc[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; // Vec3 자료형
LayoutDesc[0].AlignedByteOffset = 0;
LayoutDesc[0].InputSlot = 0;
LayoutDesc[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[0].InstanceDataStepRate = 0;
LayoutDesc[1].SematicName = "COLOR";
LayoutDesc[1].SemanticIndex = 0;
LayoutDesc[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // Vec4 자료형
LayoutDesc[1].AlignedByteOffset = 12; // POSITION의 크기가 12바이트이므로.
LayoutDesc[1].InputSlot = 0;
LayoutDesc[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
LayoutDesc[1].InstanceDataStepRate = 0;
if(FAILED(DEVICE->CreateInputLayout(LayoutDesc, 2, g_VSBlob->GetBufferPointer(), g_VSBlob->GetBufferSize(), g_Layout.GetAddressOf())))
{
assert(nullptr);
}
마지막으로 삼각형을 Render를 해주는코드
void Render()
{
// IA
UINT iStride = sizeof(Vtx);
UINT iOffset = 0;
CONTEXT->IASetVertexBuffers(0, 1, g_VB.GetAddressOf(), &iStride, &iOffset);
CONTEXT->IASetInputLayout(g_Layout.Get());
CONTEXT->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST)
CONTEXT->VSSetShader(g_VS.Get(), nullptr, 0);
CONTEXT->PSSetShader(g_PS.Get(), nullptr, 0);
CONTEXT->Draw(3, 0);
}