Direct3D11 튜토리얼 정리3: 간단한 쉐이더 활용
이전 게시글
이전 게시글에서 내용과 코드가 이어집니다. 안 보고 오셨다면
Direct3D11 튜토리얼 정리1: 화면 지우기
Direct3D11 튜토리얼 정리2: 삼각형 그리기
먼저 보시는 것도 추천!
결과
3D 관련으로 가기 전에 간단하게 요런 변환을 다뤄봅시다. 볼륨이 적은 게시글이 될 것 같네요.
녹화를 잘못했는지 검은 빈 부분이 있는데 거슬리긴 해도 일단 결과 잘 보이니 넘어가는 걸로…ㅋㅋㅋ(어차피 저만 보는 게시글이랑 다를바 없으니)
이번 게시글에서 사용할 전체 코드
Jekyll 버그인지 코드 부분 부분에서 넣지도 않은 \가 출력되고 있네요. 참고해주십쇼!
C++
#include <Windows.h>
#include <assert.h>
#include <d3d11.h>
#include <dxgi.h>
#include <DirectXMath.h>
#include <d3dcompiler.h>
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")
// 디버깅용 정적 콘솔 연결
#pragma comment(linker, "/entry:wWinMainCRTStartup /subsystem:console")
LRESULT CALLBACK WndProc(const HWND hWnd, const UINT message, const WPARAM wParam, const LPARAM lParam);
void InitializeD3D11();
void DestroyD3D11();
void Render();
HINSTANCE ghInstance;
HWND ghWnd;
const TCHAR* WINDOW_NAME = TEXT("DX11 Sample");
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
ghInstance = hInstance;
// 윈도우 클래스 정의 및 등록
WNDCLASS windowClass;
ZeroMemory(&windowClass, sizeof(WNDCLASS));
windowClass.lpfnWndProc = WndProc; // 콜백함수 등록
windowClass.lpszClassName = WINDOW_NAME; // 클래스 이름 지정
windowClass.hCursor = (HCURSOR)LoadCursor(nullptr, IDC_ARROW); // 기본 커서 지정
windowClass.hInstance = hInstance; // 클래스 인스턴스 지정
RegisterClass(&windowClass);
assert(GetLastError() == ERROR_SUCCESS);
// 실제 사용하게 될 화면 크기 지정
// 여기서는 디스플레이의 가로 / 2, 세로 / 2 사용 예정
RECT windowRect;
windowRect.left = 0;
windowRect.top = 0;
windowRect.right = GetSystemMetrics(SM_CXSCREEN) >> 1; // 디스플레이 너비 / 2
windowRect.bottom = GetSystemMetrics(SM_CYSCREEN) >> 1; // 디스플레이 높이 / 2
AdjustWindowRect(
&windowRect, // 값을 받을 RECT 구조체 주소
WS_OVERLAPPEDWINDOW, // 크기를 계산할 때 참고할 윈도우 스타일
false // 메뉴 여부
);
assert(GetLastError() == ERROR_SUCCESS);
// 윈도우 생성
ghWnd = CreateWindow(
WINDOW_NAME, // 클래스 이름
WINDOW_NAME, // 윈도우 이름
WS_OVERLAPPEDWINDOW, // 윈도우 스타일
CW_USEDEFAULT, CW_USEDEFAULT, // (x, y)
windowRect.right, windowRect.bottom, // 너비, 높이
nullptr, // 부모 윈도우 지정
nullptr, // 메뉴 지정
hInstance, // 인스턴스 지정
nullptr // 추가 메모리 지정
);
assert(GetLastError() == ERROR_SUCCESS);
InitializeD3D11(); // 윈도우 만들고 나서 D3D 초기화
// 윈도우 보여주기
ShowWindow(ghWnd, nCmdShow);
assert(GetLastError() == ERROR_SUCCESS);
// PeekMessage를 이용한 메시지 루프
MSG msg;
while (true)
{
if (PeekMessage(
&msg, // 메시지를 받을 주소
nullptr, // 메시지를 받을 윈도우
0, // 받을 메시지 범위 최솟값
0, // 받을 메시지 범위 최댓값
PM_REMOVE // 메시지 처리 방법
))
{
if (msg.message == WM_QUIT)
{
break;
}
TranslateMessage(&msg); // 메시지 문자로 해석 시도
DispatchMessage(&msg); // 메시지 전달
}
else
{
Render();
Sleep(1000 / 30);
}
}
return (int)msg.wParam;
}
// 윈도우 콜백 함수
LRESULT CALLBACK WndProc(const HWND hWnd, const UINT message, const WPARAM wParam, const LPARAM lParam)
{
switch (message)
{
case WM_DESTROY: // 윈도우 종료시 자원 해제 작업
DestroyD3D11();
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// 번거로움 피하기
using namespace DirectX;
// 사용할 정점 구조체
// 위치 정보 외에도 다양한 데이터가 추가될 수 있음
typedef struct
{
XMFLOAT3 pos; // 단순 (x, y, z)를 표현하기 위해 float3 사용
} vertex_t;
typedef struct
{
XMMATRIX transfrom; // 정점 변환용 변수
} constant_buffer_t;
// D3D11에서 사용할 변수들
// Device: 리소스 생성
// DeviceContext: 렌더링에 사용할 정보를 담을 데이터 덩어리
// SwapChain: 버퍼를 디바이스에 렌더링하고 출력
// RenderTargetView: 리소스 뷰 중에서 렌더링 대상 용도
static ID3D11Device* spDevice;
static ID3D11DeviceContext* spDeviceContext;
static IDXGISwapChain* spSwapChain;
static ID3D11RenderTargetView* spRenderTargetView;
// VertexShader: 정점 쉐이더 리소스 인터페이스
// PixelShader: 픽셀 쉐이더 리소스 인터페이스
// VertexBuffer: 정점 버퍼 리소스 인터페이스
// IndexBuffer: 인덱스 버퍼 리소스 인터페이스
static ID3D11VertexShader* spVertexShader;
static ID3D11PixelShader* spPixelShader;
static ID3D11Buffer* spVertexBuffer;
static ID3D11Buffer* spIndexBuffer;
// ConstantBuffer: 쉐이더의 전역변수용 리소스 인터페이스
static constant_buffer_t sConstantBuffer;
static ID3D11Buffer* spConstantBuffer;
void InitializeD3D11()
{
// 디바이스 생성 관련 정보
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#if DEBUG || _DEBUG
creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif /* Debug */
// 스왑체인에 대한 정보 초기화
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
// 현재 윈도우의 크기 구하기(작업 영역)
RECT windowRect;
GetClientRect(ghWnd, &windowRect);
assert(GetLastError() == ERROR_SUCCESS);
swapChainDesc.BufferCount = 1;
swapChainDesc.BufferDesc.Width = windowRect.right;
swapChainDesc.BufferDesc.Height = windowRect.bottom;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // 사용할 색상 포맷
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60; // 다시 그리는 주기의 분자
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1; // 다시 그리는 주기의 분모
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 버퍼의 용도
swapChainDesc.OutputWindow = ghWnd; // 렌더링한 결과물을 출력할 윈도우
swapChainDesc.SampleDesc.Count = 1; // 픽셀당 샘플링하는 수
swapChainDesc.SampleDesc.Quality = 0; // 이미지의 품질 수준
swapChainDesc.Windowed = true; // 창모드 설정
// 디바이스, 디바이스 컨텍스트, 스왑체인 생성
// 최신 문서는 다른 방식으로 생성 권장 중
HRESULT hr = D3D11CreateDeviceAndSwapChain(
nullptr, // 어댑터 포인터
D3D_DRIVER_TYPE_HARDWARE, // 사용할 드라이버
nullptr, // 레스터라이저의 주소
creationFlags, // 생성 플래그
nullptr, // 지원할 버전 정보 배열
0, // 버전 정보 배열의 길이
D3D11_SDK_VERSION, // D3D SDK 버전
&swapChainDesc, // 스왑체인 정보 구조체,
&spSwapChain, // 생성된 스왑체인의 포인터를 받을 주소
&spDevice, // 생성된 디바이스의 포인터를 받을 주소
nullptr, // 생성된 버전 정보를 받을 주소
&spDeviceContext // 생성된 디바이스 컨텍스트의 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 렌더 타겟으로 사용할 버퍼 갖고 오기
ID3D11Texture2D* pBackBuffer = nullptr;
hr = spSwapChain->GetBuffer(
0, // 사용할 버퍼 번호
__uuidof(ID3D11Texture2D), // 버퍼를 해석할 때 사용할 인터페이스
(void**)&pBackBuffer // 버퍼 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 렌더 타겟 뷰 생성
hr = spDevice->CreateRenderTargetView(
pBackBuffer, // 사용할 리소스 포인터
nullptr, // 렌더 타겟 뷰 정보 구조체 포인터
&spRenderTargetView // 만들어진 렌터 타겟 뷰의 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
pBackBuffer->Release();
pBackBuffer = nullptr;
// 렌더 타겟을 파이프라인 OM 단계에 설정
spDeviceContext->OMSetRenderTargets(
1, // 넣을 뷰의 수
&spRenderTargetView, // 렌터 타겟 포인터 배열
nullptr // 깊이 스텐실 뷰 포인터
);
// 뷰포트 정보 초기화
D3D11_VIEWPORT viewPort;
viewPort.Width = (FLOAT)windowRect.right;
viewPort.Height = (FLOAT)windowRect.bottom;
viewPort.MinDepth = 0.f;
viewPort.MaxDepth = 1.f;
viewPort.TopLeftX = 0;
viewPort.TopLeftY = 0;
// 뷰 포트 설정
spDeviceContext->RSSetViewports(
1, // 뷰포트의 수
&viewPort // 정보 구조체 포인터
);
// 쉐이더, 컴파일 오류를 받을 이진 개체
ID3DBlob* pVertexShaderBlob = nullptr;
ID3DBlob* pPixelShaderBlob = nullptr;
ID3DBlob* pErrorBlob = nullptr;
// 정점 쉐이더 컴파일
hr = D3DCompileFromFile(
TEXT("VertexShader.hlsl"), // 쉐이더 코드 파일 이름
nullptr, // 쉐이더 매크로를 정의하는 구조체 포인터
D3D_COMPILE_STANDARD_FILE_INCLUDE, // 쉐이더 컴파일러가 include 파일 처리에 사용하는 인터페이스 포인터
"main", // 진입점 이름
"vs_5_0", // 컴파일 대상
0, // 컴파일 옵션
0, // 컴파일 옵션2
&pVertexShaderBlob, // 컴파일된 쉐이더 데이터 포인터를 받을 주소
&pErrorBlob // 컴파일 에러 데이터 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 정점 쉐이더 리소스 생성
hr = spDevice->CreateVertexShader(
pVertexShaderBlob->GetBufferPointer(), // 컴파일된 쉐이더 데이터 포인터
pVertexShaderBlob->GetBufferSize(), // 쉐이더 데이터의 길이
nullptr, // 쉐이더 동적링크 관련 인터페이스 포인터
&spVertexShader // 정점 쉐이더 인터페이스를 받을 주소
);
assert(SUCCEEDED(hr));
// 픽셀 쉐이더 컴파일
hr = D3DCompileFromFile(
TEXT("PixelShader.hlsl"), // 쉐이더 코드 파일 이름
nullptr, // 쉐이더 매크로를 정의하는 구조체 포인터
D3D_COMPILE_STANDARD_FILE_INCLUDE, // 쉐이더 컴파일러가 include 파일 처리에 사용하는 인터페이스 포인터
"main", // 진입점 이름
"ps_5_0", // 컴파일 대상
0, // 컴파일 옵션
0, // 컴파일 옵션2
&pPixelShaderBlob, // 컴파일된 쉐이더 데이터 포인터를 받을 주소
&pErrorBlob // 컴파일 에러 데이터 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 픽셀 쉐이더 리소스 생성
hr = spDevice->CreatePixelShader(
pPixelShaderBlob->GetBufferPointer(), // 컴파일된 쉐이더 데이터 포인터
pPixelShaderBlob->GetBufferSize(), // 쉐이더 데이터의 길이
nullptr, // 쉐이더 동적링크 관련 인터페이스 포인터
&spPixelShader // 정점 쉐이더 인터페이스를 받을 주소
);
assert(SUCCEEDED(hr));
// GPU에게 정점의 정보를 알려주기 위한 구조체 초기화
// 구조체에 포함된 각 요소별로 이 작업을 진행해서 넘겨줘야함
D3D11_INPUT_ELEMENT_DESC vertexLayoutDesc;
vertexLayoutDesc.SemanticName = "POSITION"; // 해당 데이터의 용도
vertexLayoutDesc.SemanticIndex = 0; // 용도가 겹칠 경우 사용할 색인 번호
vertexLayoutDesc.Format = DXGI_FORMAT_R32G32B32_FLOAT; // 입력 데이터 형식
vertexLayoutDesc.InputSlot = 0; // 버퍼의 슬롯 번호
vertexLayoutDesc.AlignedByteOffset = 0; // 구조체에서 요소의 시작 위치(바이트 단위)
vertexLayoutDesc.InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; // 입력 데이터의 분류법
vertexLayoutDesc.InstanceDataStepRate = 0; // 인스턴싱에 사용할 배수
// 파이프라인에 전송할 레이아웃 배열
D3D11_INPUT_ELEMENT_DESC layoutArr[] = {
vertexLayoutDesc
};
// 파이프라인에서 사용할 레이아웃 생성
ID3D11InputLayout* pVertexLayout = nullptr;
hr = spDevice->CreateInputLayout(
layoutArr, // 레이아웃 배열
ARRAYSIZE(layoutArr), // 배열의 길이
pVertexShaderBlob->GetBufferPointer(), // 컴파일된 쉐이더 데이터 포인터
pVertexShaderBlob->GetBufferSize(), // 쉐이더 데이터의 길이
&pVertexLayout // 생성된 레이아웃 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 정점 레이아웃 설정
spDeviceContext->IASetInputLayout(pVertexLayout);
pVertexShaderBlob->Release();
pVertexShaderBlob = nullptr;
pPixelShaderBlob->Release();
pPixelShaderBlob = nullptr;
pVertexLayout->Release();
pVertexLayout = nullptr;
// 전송할 정점 배열
vertex_t vertices[3] = {
DirectX::XMFLOAT3(0.f, 0.5f, 0.f),
DirectX::XMFLOAT3(0.5f, -0.5f, 0.f),
DirectX::XMFLOAT3(-0.5f, -0.5f, 0.f)
};
// 정점 버퍼에 대한 정보 구조체 초기화
D3D11_BUFFER_DESC bufferDesc;
bufferDesc.ByteWidth = sizeof(vertices); // 버퍼의 바이트 크기
bufferDesc.Usage = D3D11_USAGE_DEFAULT; // 버퍼의 용도
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 파이프라인에 뭘로 바인딩 할지
bufferDesc.CPUAccessFlags = 0; // CPU의 접근 권한
bufferDesc.MiscFlags = 0; // 리소스에 대한 옵션
bufferDesc.StructureByteStride = sizeof(vertex_t); // 각 요소별 바이트 크기
// 초기화할 정점 버퍼 서브리소스 구조체
D3D11_SUBRESOURCE_DATA vertexSubresource;
vertexSubresource.pSysMem = vertices; // 전송할 데이터 포인터
vertexSubresource.SysMemPitch = 0; // 다음 행으로 가기 위한 시스템 바이트 수
vertexSubresource.SysMemSlicePitch = 0; // 다음 면으로 가기 위한 시스템 바이트 수
// 정점 버퍼 생성
hr = spDevice->CreateBuffer(
&bufferDesc, // 버퍼 정보 구조체 포인터
&vertexSubresource, // 정점 서브 리소스 포인터
&spVertexBuffer // 만들어진 버퍼 리소스 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 파이프라인에 정점 버퍼 설정
UINT stride = sizeof(vertex_t);
UINT offset = 0;
spDeviceContext->IASetVertexBuffers(
0, // 슬롯 번호
1, // 버퍼의 수
&spVertexBuffer, // 정점 버퍼 주소
&stride, // 요소별 크기 배열
&offset // 각 버퍼별 시작 오프셋 배열
);
// 삼각형 그리는 방법 설정
spDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// 인덱스 데이터 정의
WORD indice[3] = {
0, 1, 2
};
// 인덱스 버퍼에 대한 정보 구조체 초기화
D3D11_BUFFER_DESC indexBufferDesc;
indexBufferDesc.ByteWidth = sizeof(indice); // 버퍼의 바이트 크기
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; // 버퍼의 용도
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; // 파이프라인에 뭘로 바인딩 할지
indexBufferDesc.CPUAccessFlags = 0; // CPU의 접근 권한
indexBufferDesc.MiscFlags = 0; // 리소스에 대한 옵션
indexBufferDesc.StructureByteStride = sizeof(WORD); // 각 요소별 바이트 크기
// 초기화할 인덱스 버퍼 서브리소스 구조체
D3D11_SUBRESOURCE_DATA indexSubresource;
indexSubresource.pSysMem = indice; // 전송할 데이터 포인터
indexSubresource.SysMemPitch = 0; // 다음 행으로 가기 위한 시스템 바이트 수
indexSubresource.SysMemSlicePitch = 0; // 다음 면으로 가기 위한 시스템 바이트 수
// 인덱스 버퍼 생성
hr = spDevice->CreateBuffer(
&indexBufferDesc, // 버퍼 정보 구조체 포인터
&indexSubresource, // 인덱스 서브 리소스 포인터
&spIndexBuffer // 만들어진 버퍼 리소스 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 파이프라인에 인덱스 버퍼 설정
spDeviceContext->IASetIndexBuffer(
spIndexBuffer, // 설정할 인덱스 버퍼 포인터
DXGI_FORMAT_R16_UINT, // 인덱스 버퍼의 형식
0 // 시작 오프셋
);
// 회전에 사용할 변수 초기화
sConstantBuffer.transfrom = XMMatrixIdentity();
// 상수 버퍼에 대한 정보 구조체 초기화
D3D11_BUFFER_DESC constantBufferDesc;
constantBufferDesc.ByteWidth = sizeof(sConstantBuffer); // 버퍼의 바이트 크기
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT; // 버퍼의 용도
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; // 파이프라인에 뭘로 바인딩 할지
constantBufferDesc.CPUAccessFlags = 0; // CPU의 접근 권한
constantBufferDesc.MiscFlags = 0; // 리소스에 대한 옵션
constantBufferDesc.StructureByteStride = sizeof(sConstantBuffer.transfrom); // 각 요소별 바이트 크기
// 초기화할 상수 버퍼 서브리소스 구조체
D3D11_SUBRESOURCE_DATA constantBufferSubresource;
constantBufferSubresource.pSysMem = &sConstantBuffer; // 전송할 데이터 포인터
constantBufferSubresource.SysMemPitch = 0; // 다음 행으로 가기 위한 시스템 바이트 수
constantBufferSubresource.SysMemSlicePitch = 0; // 다음 면으로 가기 위한 시스템 바이트 수
// 인덱스 버퍼 생성
hr = spDevice->CreateBuffer(
&constantBufferDesc, // 버퍼 정보 구조체 포인터
&constantBufferSubresource, // 상수 버퍼 서브 리소스 포인터
&spConstantBuffer // 만들어진 버퍼 리소스 포인터를 받을 주소
);
// 상수 버퍼 설정
spDeviceContext->VSSetConstantBuffers(
0, // 시작 슬롯
1, // 설정할 버퍼의 수
&spConstantBuffer // 설정할 버퍼 리소스 배열
);
spDeviceContext->PSSetConstantBuffers(
0, // 시작 슬롯
1, // 설정할 버퍼의 수
&spConstantBuffer // 설정할 버퍼 리소스 배열
);
}
void DestroyD3D11()
{
if (spConstantBuffer != nullptr)
{
spConstantBuffer->Release();
spConstantBuffer = nullptr;
}
if (spIndexBuffer != nullptr)
{
spIndexBuffer->Release();
spIndexBuffer = nullptr;
}
if (spVertexBuffer != nullptr)
{
spVertexBuffer->Release();
spVertexBuffer = nullptr;
}
if (spPixelShader != nullptr)
{
spPixelShader->Release();
spPixelShader = nullptr;
}
if (spVertexShader != nullptr)
{
spVertexShader->Release();
spVertexShader = nullptr;
}
if (spRenderTargetView != nullptr)
{
spRenderTargetView->Release();
spRenderTargetView = nullptr;
}
if (spSwapChain != nullptr)
{
spSwapChain->Release();
spSwapChain = nullptr;
}
if (spDeviceContext != nullptr)
{
spDeviceContext->ClearState();
spDeviceContext->Release();
spDeviceContext = nullptr;
}
if (spDevice != nullptr)
{
spDevice->Release();
spDevice = nullptr;
}
}
void Render()
{
const FLOAT clearColor[4] = { 209 / 255.f, 95 / 255.f, 238 / 255.f, 1.f };
assert(spRenderTargetView != nullptr);
spDeviceContext->ClearRenderTargetView(
spRenderTargetView, // 대상 렌더 타겟 뷰
clearColor // 채워 넣을 색상
);
// 쉐이더 설정
spDeviceContext->VSSetShader(
spVertexShader, // 설정할 쉐이더 인터페이스 포인터
nullptr, // HLSL 클래스를 캡슐화하는 인터페이스 포인터 배열
0 // 배열의 길이
);
spDeviceContext->PSSetShader(
spPixelShader, // 설정할 쉐이더 인터페이스 포인터
nullptr, // HLSL 클래스를 캡슐화하는 인터페이스 포인터 배열
0 // 배열의 길이
);
// 다음 변환 가중치 계산
sConstantBuffer.transfrom *= XMMatrixRotationZ(XM_PI / 36);
// HLSL 계산 방식으로 인한 전치
XMMATRIX trTransform = XMMatrixTranspose(sConstantBuffer.transfrom);
// 바인딩한 리소스 업데이트
spDeviceContext->UpdateSubresource(
spConstantBuffer, // 업데이트할 서브리소스 포인터
0, // 업데이트할 서브리소스 번호
nullptr, // 서브리소스 선택 박스 포인터
&trTransform, // 업데이트에 사용할 데이터
0, // 다음 행까지의 시스템 메모리 바이트 수
0 // 다음 깊이까지의 시스템 메모리 바이트 수
);
spDeviceContext->DrawIndexed(
3, // 그릴 인덱스의 수
0, // 첫 인덱스를 읽을 위치
0 // 정점 버퍼로부터 정점을 읽기 전에 각 인덱스에 더할 값
);
spSwapChain->Present(
1, // 동기화에 사용할 시간
0 // 프레임 표현 옵션
);
}
HLSL
VertexShader.hlsl
#include "ShaderHeader.hlsli"
float4 main(float4 pos : POSITION) : SV_POSITION
{
return mul(pos, transform);
}
PixelShader.hlsl
#include "ShaderHeader.hlsli"
float4 main(float4 pos : SV_Position) : SV_TARGET
{
return mul(float4(pos.x, pos.y, pos.z, 1.f), transform);
}
ShaderHeader.hlsli
cbuffer ConstantBuffer : register(b0)
{
float4x4 transform;
}
코드 살펴보기
사용할 라이브러리 및 컴파일러 옵션
이전과 동일
윈도우 관련 코드
이전과 동일
Direct3D11
이번 흐름은 다음과 같습니다.
- 상수 버퍼(Constant Buffer)용 구조체 선언
- 튜토리얼1, 2와 동일한 방식
- 상수 버퍼용 개체 포인터 선언
- 상수 버퍼 데이터 정의
- 상수 버퍼 리소스 생성 및 상수 버퍼를 사용할 쉐이더에 설정
- 상수 버퍼 업데이트 및 렌더링
상수 버퍼(Constant Buffer)용 구조체 선언
typedef struct
{
XMMATRIX transfrom; // 정점 변환용 변수
} constant_buffer_t;
이번 게시글에서는 요것만 사용할 예정입니다. 결과에서 계속 색깔 바뀌면서 회전 했던 것 치곤 별 거 없죠?ㅋㅋ 하지만 실제로는
typedef struct
{
// 행렬1 선언
// 행렬2 선언
// 행렬3 선언
// 공통 상수 선언
// ...
} constant_buffer_t;
요런 식으로 꽤나 많은 정보를 넣어서 사용하긴 합니다. 추가적인 정보가 있는데 그건 차후에 말씀드립죠!
상수 버퍼(Constant Buffer)
일단 주 참고자료 설명으로는 어플리케이션이 쉐이더에게 넘길 데이터를 저장하는 장소 정도로 설명이 돼있네요. D3D11 버퍼 문서에서는 쉐이더 상수 데이터를 파이프라인에 효율적으로 공급하는 걸 도와주는 버퍼 정도로 나와있습니다. 그냥 한마디로 쉐이더에서 쓸 전역변수를 저장하는 곳이다 정도로 생각하면 좋을 것 같습니다.
상수 버퍼용 개체 포인터 선언
// ConstantBuffer: 쉐이더의 전역변수용 리소스 인터페이스
static constant_buffer_t sConstantBuffer;
static ID3D11Buffer* spConstantBuffer;
사용할 상수 버퍼 및 상수 버퍼 리소스를 선언해줍시다. 제가 아직 방법을 모르는 걸 수도 있는데 상수 버퍼는 내용 변경이 필요한 경우 CPU에서 업데이트한 내용을 전송해서 사용하는터라 CPU에서 쓸 자리도 같이 잡는 게 보통이더군요. 여튼 그래서 sConstantBuffer
도 같이 선언됩니다.
상수 버퍼 데이터 정의
// 회전에 사용할 변수 초기화
sConstantBuffer.transfrom = XMMatrixIdentity();
// 상수 버퍼에 대한 정보 구조체 초기화
D3D11_BUFFER_DESC constantBufferDesc;
constantBufferDesc.ByteWidth = sizeof(sConstantBuffer); // 버퍼의 바이트 크기
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT; // 버퍼의 용도
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; // 파이프라인에 뭘로 바인딩 할지
constantBufferDesc.CPUAccessFlags = 0; // CPU의 접근 권한
constantBufferDesc.MiscFlags = 0; // 리소스에 대한 옵션
constantBufferDesc.StructureByteStride = sizeof(sConstantBuffer.transfrom); // 각 요소별 바이트 크기
// 초기화할 상수 버퍼 서브리소스 구조체
D3D11_SUBRESOURCE_DATA constantBufferSubresource;
constantBufferSubresource.pSysMem = &sConstantBuffer; // 전송할 데이터 포인터
constantBufferSubresource.SysMemPitch = 0; // 다음 행으로 가기 위한 시스템 바이트 수
constantBufferSubresource.SysMemSlicePitch = 0; // 다음 면으로 가기 위한 시스템 바이트 수
다른 버퍼랑 또오옥같습니다. 플래그만 잘 설정해주면 사실상 끝
제가 다른 버퍼 코드 복붙에서 값 넣어놔서 그렇긴 한데 코드는 더 덜어내서 써도 되긴 합니다.
상수 버퍼 리소스 생성 및 상수 버퍼를 사용할 쉐이더에 설정
// 인덱스 버퍼 생성
hr = spDevice->CreateBuffer(
&constantBufferDesc, // 버퍼 정보 구조체 포인터
&constantBufferSubresource, // 상수 버퍼 서브 리소스 포인터
&spConstantBuffer // 만들어진 버퍼 리소스 포인터를 받을 주소
);
// 상수 버퍼 설정
spDeviceContext->VSSetConstantBuffers(
0, // 시작 슬롯
1, // 설정할 버퍼의 수
&spConstantBuffer // 설정할 버퍼 리소스 배열
);
spDeviceContext->PSSetConstantBuffers(
0, // 시작 슬롯
1, // 설정할 버퍼의 수
&spConstantBuffer // 설정할 버퍼 리소스 배열
);
여기도 뻔하죠?
역시나
void DestroyD3D11()
{
if (spConstantBuffer != nullptr)
{
spConstantBuffer->Release();
spConstantBuffer = nullptr;
}
// 생략
}
해제 잘 해줍시다!
상수 버퍼 업데이트 및 렌더링
void Render()
{
// 생략
// 다음 변환 가중치 계산
sConstantBuffer.transfrom *= XMMatrixRotationZ(XM_PI / 36);
// HLSL 계산 방식으로 인한 전치
XMMATRIX trTransform = XMMatrixTranspose(sConstantBuffer.transfrom);
// 바인딩한 리소스 업데이트
spDeviceContext->UpdateSubresource(
spConstantBuffer, // 업데이트할 서브리소스 포인터
0, // 업데이트할 서브리소스 번호
nullptr, // 서브리소스 선택 박스 포인터
&trTransform, // 업데이트에 사용할 데이터
0, // 다음 행까지의 시스템 메모리 바이트 수
0 // 다음 깊이까지의 시스템 메모리 바이트 수
);
// 생략
}
상수 버퍼 내용을 다음 변환으로 업데이트하고 전송하는 부분만 적어놨습니다. 개인적으론 CPU에서 계산해서 넘겨주는 방식이 조금 독특하다 느껴지더군요ㅋㅋ
주의할 점
행렬, 벡터 사용할 때 주의할 점이 있습니다. 기본적으로 DirectX랑, HLSL의 ‘사용법’ 자체는 벡터 * 행렬(Row-major 방식)로 구성되어 있고 DirectXMath 라이브러리도 동일한 방법으로 구현돼있습니다. 그러나 HLSL은 기본 방식은 행렬 * 벡터(Column-major 방식) 형태로 되어 있기 때문에 전송하기 전에 전치 연산을 하고 전송해주셔야 합니다.(따로 설정하는 방법도 있긴 합니다. 자세한 건 문서 참고!)
Row-major는 요렇게 표현 하는 방법이고
Column-major는 요렇게 표현합니다.
사소한 팁?
앞서 나중에 보자던 정보와 관련된 내용입니다. 앞서 말씀드렸던 것 처럼 상수 버퍼는
typedef struct
{
// 행렬1 선언
// 행렬2 선언
// 행렬3 선언
// 공통 상수 선언
// ...
} constant_buffer_t;
요렇게 만든 정보를 담고 전달될 수 있다고 했었습니다. 그런데 만약 저 상수 버퍼 중에서 변하는 정보는 몇 개 없는 상태라면 저 덩치 큰 상수 버퍼를 계속 다시 전송해주는 게 효율적이지 않겠죠? 마치 정점 버퍼에서 데이터를 직접 전송하는 것보다는 인덱스 버퍼를 만들어서 전송하는 게 더 낫다는 느낌과 비슷하다고도 볼 수 있습니다. 그렇기 때문에 효율적인 전송이 필요한 경우
typedef struct
{
// 자주 바뀌는 행렬1
} constant_buffer1_t;
typedef struct
{
// 자주 바뀌는 행렬2
} constant_buffer2_t;
typedef struct
{
// 자주 바뀌는 행렬3
} constant_buffer3_t;
typedef struct
{
// 공통 상수 선언
// ...
} constant_buffer_t;
이런 식으로 쪼개서 데이터를 만들어 두고 각각에 대해서 상수 버퍼를 만들어서 쉐이더에 설정합니다. 그러고 나서 쉐이더 코드에서
cbuffer cbMatrix1
{
// 행렬1
}
cbuffer cbMatrix2
{
// 행렬2
}
cbuffer cbMatrix3
{
// 행렬3
}
이런 식으로 각자 잡아 놓고 사용하기도 합니다. 당연히 각자 잡아놓은 만큼 UpdateResource()
도 따로 해줘야 하는 귀찮음은 있긴 합니다. 그러나 효율적으로 전송을 하고 싶다면 고려해야될 방법이기도 합니다.
HLSL
제가 HLSL에 관해선 별다른 내용을 넣고 있지 않은데 이유가
- C-like 언어라 현재 C++를 사용하고 있는 상황에서 문법적으론 크게 문제가 되지 않는다.
- 쉐이더로 아주아주 복잡한 프로그램을 짜는 건 아니라서 검색하면서 해결하기에 무리가 없을 것 같다.
라고 생각하기 때문입니다. 물론 제가 전문가는 아니라 틀린 생각일 수도 있음은 참고 부탁드려요ㅋㅋ
Semantics
그래도 이 용어 정도는 짚고 가면 좋을 것 같아서 언급하고 갑니다. 주로 자료형 같은 거 옆에 ‘:’(콜론)을 붙여서 다는 키워드들 말하는데 문서에서는 파라미터의 의도된 용도에 대해서 정보를 전달하려고 쉐이더 입력 또는 출력에 첨부되는 문자열 정도로 설명하고 있네요.
VertexShader.hlsl
cbuffer ConstantBuffer : register(b0)
{
float4x4 transform;
}
float4 main(float4 pos : POSITION) : SV_POSITION
{
return mul(pos, transform);
}
PixelShader.hlsl
cbuffer ConstantBuffer : register(b0)
{
float4x4 transform;
}
float4 main(float4 pos : SV_Position) : SV_TARGET
{
return mul(float4(pos.x, pos.y, pos.z, 1.f), transform);
}
쉐이더 코드 간단하죠? 이런 간단한 코드로도 뭔가 하는 것처럼 보이는 결과를 만들 수 있습니다ㅋㅋ
그런데 hlsl 파일이 별도인데 ConstantBuffer 코드가 복붙으로 중복돼서 불-편한 상황이 아닐 수가 없습니다. 그런데 다행히도 HLSL은 include를 지원합니다!
HLSL의 include를 활용한 DirectX 및 HLSL
안타깝게도 바로 include한다고 사용할 수는 없습니다 흑흑…
VertexShader.hlsl
#include "ShaderHeader.hlsli"
float4 main(float4 pos : POSITION) : SV_POSITION
{
return mul(pos, transform);
}
PixelShader.hlsl
#include "ShaderHeader.hlsli"
float4 main(float4 pos : SV_Position) : SV_TARGET
{
return mul(float4(pos.x, pos.y, pos.z, 1.f), transform);
}
ShaderHeader.hlsli
cbuffer ConstantBuffer : register(b0)
{
float4x4 transform;
}
C랑 비슷하니 어려울 건 없죠? 일단 쉐이더 코드가 간단하니 요것부터 보고
InitializeD3D11
// 생략
// 정점 쉐이더 컴파일
hr = D3DCompileFromFile(
TEXT("VertexShader.hlsl"), // 쉐이더 코드 파일 이름
nullptr, // 쉐이더 매크로를 정의하는 구조체 포인터
D3D_COMPILE_STANDARD_FILE_INCLUDE, // 쉐이더 컴파일러가 include 파일 처리에 사용하는 인터페이스 포인터
"main", // 진입점 이름
"vs_5_0", // 컴파일 대상
0, // 컴파일 옵션
0, // 컴파일 옵션2
&pVertexShaderBlob, // 컴파일된 쉐이더 데이터 포인터를 받을 주소
&pErrorBlob // 컴파일 에러 데이터 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
// 생략
// 픽셀 쉐이더 컴파일
hr = D3DCompileFromFile(
TEXT("PixelShader.hlsl"), // 쉐이더 코드 파일 이름
nullptr, // 쉐이더 매크로를 정의하는 구조체 포인터
D3D_COMPILE_STANDARD_FILE_INCLUDE, // 쉐이더 컴파일러가 include 파일 처리에 사용하는 인터페이스 포인터
"main", // 진입점 이름
"ps_5_0", // 컴파일 대상
0, // 컴파일 옵션
0, // 컴파일 옵션2
&pPixelShaderBlob, // 컴파일된 쉐이더 데이터 포인터를 받을 주소
&pErrorBlob // 컴파일 에러 데이터 포인터를 받을 주소
);
assert(SUCCEEDED(hr));
ID3DInclude 포인터 자리에 저렇게 옵션 넣어주면 사용가능합니다! 사실 별 거 없었습니다ㅋㅋㅋ 근데 포인터 자리에 저렇게 플래그 같아보이는 옵션을 넣는 게 어색한 느낌도 있을 거 같아서 뜸 들여봤습니다ㅎㅎ