루키스님의 인프런 강의를 보고 정리한 내용입니다.
https://www.inflearn.com/course/directx11-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C-%EB%8F%84%EC%95%BD%EB%B0%98/dashboard
DirectX 11 환경에서 삼각형을 화면에 그리는 과정에 대해 알아보겠습니다.
기본적인 도형을 화면에 출력하기 위해서도 꼭 필요한 과정들을 거쳐야 합니다.
Types.h
// 정점에 대한 정보를 저장하기 위한 구조체를 선언합니다. struct Vertex { // 위치 좌표 (x, y, z) Vec3 position; // 색상 정보 (R, G, B, A) Color color; };
Game.h
private: // 기하학적인 도형을 만들기 위한 함수를 선언합니다. void CreateGeometry() private: // Geometry // * 정점들을 저장하기 위한 벡터 컨테이너를 선언합니다. vector<Vertex> _vertices; // _vertices의 데이터들은 CPU의 메모리에 존재합니다. // GPU의 메모리(VRAM)으로 옮겨줄 필요가 있습니다. // * 정점 정보들을 저장하기 위한 버퍼를 선언합니다. ComPtr<ID3D11Buffer> _vertexBuffer = nullptr;
Game.cpp
void Game::CreateGeometry() { // 3각형을 위한 Vertex Data { // 사이즈 조정 _vertices.resize(3); // 위치 정보와 색상 정보를 설정합니다. _vertices[0].position = Vec3(-0.5f, -0.5f, 0.0f); _vertices[0].color = Color(1.0f, 0.0f, 0.0f, 0.0f); _vertices[1].position = Vec3(0.0f, 0.5f, 0.0f); _vertices[1].color = Color(1.0f, 0.0f, 0.0f, 0.0f); _vertices[2].position = Vec3(0.5f, -0.5f, 0.0f); _vertices[2].color = Color(1.0f, 0.0f, 0.0f, 0.0f); } // VertexBuffer { // 버퍼 생성에 사용될 DESC를 생성합니다. D3D11_BUFFER_DESC desc; // 0으로 초기화해줍니다. ZeroMemory(&desc, sizeof(desc)); // * D3D11_USAGE_IMMUTABLE : GPU만 읽을 수 있는 방식으로 사용하겠다고 설정 desc.Usage = D3D11_USAGE_IMMUTABLE; // * D3D11_BIND_VERTEX_BUFFER : VertexBuffer를 바인딩하는 용도로 사용하겠다고 설정 desc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // * desc의 바이트 크기를 설정합니다. (데이터 유형의 크기 * 수) desc.ByteWidth = (uint32)sizeof(Vertex) * _vertices.size(); D3D11_SUBRESOURCE_DATA data; ZeroMemory(&data, sizeof(data)); // 첫 번째 데이터의 시작 주소를 저장합니다. data.pSysMem = _vertices.data(); // 버퍼를 생성해줍니다. (_vertexBuffer에 결과물을 저장합니다.) _device->CreateBuffer(&desc, &data, _vertexBuffer.GetAddressOf()); // -> 위 과정을 거쳐 GPU쪽에 버퍼가 생성되며 초기에 _vertices 값들이 복사됩니다. // -> 즉, 이 과정 이후부터는 GPU만 Read Only로 사용하겠다는 의미입니다. } }
위에서 만든 VertexBuffer의 정보들이 어떻게 되어있는지 묘사해야 하는 과정이 필요합니다.
즉, 입력 버퍼 데이터를 설명하는 입력 레이아웃이 필요합니다.
→ 하지만 입력 레이아웃을 생성하기 위해서는 먼저 셰이더에 대한 정보가 필요합니다.
→ 꼭짓점 셰이더 파일을 생성합니다.
셰이더를 사용하는 방법은 두 가지가 존재합니다.
1번과 2번 방법을 모두 사용하도록 합니다.
→ 빌드하는 것을 체크하지 않으면 hlsl 파일의 오류를 컴파일 타임에서 잡아주지 않음
Vertax Shader, Pixel Shader를 한 파일에 묶어서 간단히 설명하겠습니다.
Default.hlsl
// VertexShader에 들어오는 구조체를 생성합니다. struct VS_INPUT { // 생성할 Input Layout을 보면 // position은 POSITION으로, color는 COLOR로 읽기로 지정 float4 position : POSITION; float4 color : COLOR; }; // VertexShader에서 출력되는 구조체를 생성합니다. struct VS_OUTPUT { // SV : System Value - 무조건 있어야 하는 값 float4 position : SV_POSITION; float4 color : COLOR; }; // VertexShader의 메인 함수를 정의합니다. // * VS_OUTPUT : 리턴 타입 // * VS : 함수 명 // * (VS_INPUT input) : 입력 매개변수 VS_OUTPUT VS(VS_INPUT input) { // 복잡한 연산 과정이 필요하지만 지금은 간단히 return 시켜줍니다. // * 출력 타입의 구조체를 생성합니다. VS_OUTPUT output; // * 출력할 구조체의 정보를 채워줍니다. output.position = input.position; output.color = input.color; // * 만들어진 구조체를 반환합니다. return output; }
// 렌더링 파이프라인이 시작되면 IA단계에서 VS로 넘어가게 되는데 이 때 위에서 선언한 메인 함수로 들어온다고 생각하면 됩니다. // * VS 단계의 메인 함수에서는 무조건 정점 단위로 실행이 됩니다. // 렌더링 파이프라인 // * IA - VS - RS - PS - OM (핵심단계들) // -> 위 단계들은 포맷(Input Layout)에 맞게 기하 도형들이 넘어온다고 생각하면 됩니다. // IA : 입력 // VS : 정점 단위로 연산 // RS : 지금은 삼각형을 만들거라고 입력했기 때문에 3개의 정점 사이 영역만 인지하고, 아닌 영역은 제거해줍니다. // + 각 정점의 색상이 다르면 중앙 위치의 색은 기본적으로 정점들의 색을 섞어서 보간해 결과를 만들어줍니다. // PS : RS로부터 받은 정보들 토대로 조명(색상)과 관련된 연산을 수행 // VS(정점) -> RS(처리) -> PS(픽셀)
// PixelShader의 메인 함수를 정의합니다. // * (VS_OUTPUT input) : VS 단계에서 리턴해준 output을 입력 파라미터로 전달 받습니다. // * SV_Target : PS의 결과물이 SV_Target(렌더 타겟)에 전달합니다. float4 PS(VS_OUTPUT input) : SV_Target { // 조명(색상)과 관련된 연산 처리 후 반환해주는 로직이 들어가는 자리입니다. // * 지금은 테스트로 빨간 삼각형을 그리기 때문에 빨간색을 반환한다고 가정합니다. return float4(1, 0, 0, 0); }
Game.h
private: // 셰이더는 파일을 로드하는 방식으로 만들어줘야 합니다. // * 공용으로 셰이더를 파일로부터 로드하기 위한 함수를 선언합니다. // * path : 경로 // * name : 이름 // * version : 셰이더 버전 // * Blob : 로드한 셰이더 결과물을 저장할 변수 void LoadShaderFromFile(const wstring& path, const string& name, const string& version, ComPtr<ID3DBlob>& blob); // VS // * VS를 로드해 저장하기 위한 변수를 선언합니다. ComPtr<ID3D11VertexShader> _vertexShader = nullptr; // * 임의 길이 데이터를 반환하는 데 사용할 변수를 선언합니다. ComPtr<ID3DBlob> _vsBlob; // PS // * PS를 로드해 저장하기 위한 변수를 선언합니다. ComPtr<ID3D11PixelShader> _pixelShader = nullptr; // * 임의 길이 데이터를 반환하는 데 사용할 변수를 선언합니다. ComPtr<ID3DBlob> _psBlob;
Game.cpp
void Game::LoadShaderFromFile(const wstring& path, const string& name, const string& version, ComPtr<ID3DBlob>& blob) { // 비트 플래그 설정 (Debug 모드, 최적화 건너 뛰기) const uint32 complieFlag = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; // HLSL(High Level Shader Language) 코드를 지정된 대상에 대한 바이트코드로 컴파일합니다. HRESULT hr = ::D3DCompileFromFile( path.c_str(), nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, name.c_str(), version.c_str(), complieFlag, 0, blob.GetAddressOf(), nullptr ); CHECK(hr); }
Game.h
private: // VS를 생성하기 위한 함수를 선언합니다. void CreateVS(); // PS를 생성하기 위한 함수를 선언합니다. void CreatePS();
Game.cpp
void Game::CreateVS() { // 만든 셰이더 파일로부터 _vsBlob에 임시 데이터를 불러와 로드합니다. LoadShaderFromFile(L"Default.hlsl", "VS", "vs_5_0", _vsBlob); // VS를 생성합니다. HRESULT hr = _device->CreateVertexShader(_vsBlob->GetBufferPointer(), _vsBlob->GetBufferSize(), nullptr, _vertexShader.GetAddressOf()); CHECK(hr); } void Game::CreatePS() { // 만든 셰이더 파일로부터 _psBlob에 임시 데이터를 불러와 로드합니다. LoadShaderFromFile(L"Default.hlsl", "PS", "ps_5_0", _vsBlob); // PS를 생성합니다. HRESULT hr = _device->CreatePixelShader(_psBlob->GetBufferPointer(), _psBlob->GetBufferSize(), nullptr, _pixelShader.GetAddressOf()); CHECK(hr); }
Game.h
private: // 생성한 기하학적인 도형이 어떻게 되어있는지 묘사해야 합니다. // * input layout을 생성하기 위한 함수를 선언합니다. void CreateInputLayout(); // * 입력 레이아웃을 저장하기 위한 변수를 선언합니다. ComPtr<ID3D11InputLayout> _inputLayout = nullptr;
Game.cpp
void Game::CreateInputLayout() { // 입력 요소에 대한 정보를 생성합니다. // * Vertex 구조체의 내부 요소들에 대해 묘사합니다. D3D11_INPUT_ELEMENT_DESC layout[] = { {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; // layout의 개수를 저장합니다. const int32 count = sizeof(layout) / sizeof(D3D11_INPUT_ELEMENT_DESC); // 입력 버퍼 데이터를 설명하는 입력 레이아웃 개체를 만듭니다. _device->CreateInputLayout(layout, count, _vsBlob->GetBufferPointer(), _vsBlob->GetBufferSize(), _inputLayout.GetAddressOf()); }
Game.cpp
void Game::Init(HWND hwnd) { // 멤버 변수 초기화 _hwnd = hwnd; _width = GWinSizeX; _height = GWinSizeY; CreateDeviceAndSwapChain(); CreateRenderTargetView(); SetViewport(); // 삼각형 출력하기 실습 CreateGeometry(); CreateVS(); CreateInputLayout(); CreatePS(); } void Game::Render() { // 렌더를 위한 준비 작업 RenderBegin(); // TODO : 렌더 // IA - VS - RS - PS - OM { // IA { // * stride : Vertex 구조체의 크기 uint32 stride = sizeof(Vertex); // * offset : 보간 수치 uint32 offset = 0; // 디바이스 컨텍스트를 이용해 IA에 정점 버퍼를 연결시켜줍니다. _deviceContext->IASetVertexBuffers(0, 1, _vertexBuffer.GetAddressOf(), &stride, &offset); // 디바이스 컨텍스트를 이용해 IA에 InputLayout 정보를 연결시켜줍니다. _deviceContext->IASetInputLayout(_inputLayout.Get()); // 삼각형(대부분 모든 사물은 삼각형으로 표현)을 그리는 과정에서 전달한 정점들을 어떤 순서로 이어 붙일 것인지에 대한 정보를 지정합니다. (topology) _deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); } // VS { // 디바이스 컨텍스트를 이용해 VS에 만든 셰이더를 연결시켜줍니다. _deviceContext->VSSetShader(_vertexShader.Get(), nullptr, 0); } // RS { } // PS { // 디바이스 컨텍스트를 이용해 PS에 만든 셰이더를 연결시켜줍니다. _deviceContext->PSSetShader(_pixelShader.Get(), nullptr, 0); } // OM { } // 디바이스 컨텍스트를 이용해 해당 물체를 그려달라고 요청합니다. _deviceContext->Draw(_vertices.size(), 0); } // 최종 렌더 정보를 제출 RenderEnd(); }
실행 결과 (좌 : 기본 테스트 | 우 : RS에 의한 색상 혼합 테스트)