-
[DirectX] 4. 삼각형 띄우기Game Development/DirectX 2024. 6. 26. 18:14
이번 글에서는 삼각형을 한번 화면에 띄워 보도록 하겠습니다.
삼각형을 만들기 위해서는 메쉬를 표현하는 Mesh 클래스를 제작해야 합니다.
여기서 메쉬는 오브젝트를 구성하는 정점들이 이루는 다각형의 집합입니다.
이 메시를 통해서 오브젝트를 띄울 수 있기 때문에 Mesh 클래스를 먼저 제작하는 것입니다.
Mesh는 결국 정점들에 의해 이루어져 있기 때문에
정점을 표현하는 Vertex 클래스도 함께 제작하도록 하겠습니다.
// Vertex.h #pragma once class Vertex { public: Vertex(); Vertex(float x, float y, float z); // 정점의 좌표 float x, y, z; };
// Vertex.cpp #include "Vertex.h" Vertex::Vertex() : Vertex(0.0f, 0.0f, 0.0f) { } Vertex::Vertex(float x, float y, float z) : x(x), y(y), z(z) { }
// Mesh.h #pragma once #include "Vertex.h" #include <wrl.h> #include <d3d11.h> using Mircosoft::WRL::ComPtr; class Mesh { public: Mesh(); bool InitBuffers(ID3D11Device* device, ID3DBlob* vsBuffer); void RenderBuffers(ID3D11DeviceContext* deviceContext); private: int vertexCount; // 버퍼 리소스에 액세스하기 위한 인터페이스 ComPtr<ID3D11Buffer> vertexBuffer; // 정점이 어떤 용도인지 DirectX에게 알려주는 역할을 한다. ComPtr<ID3D11InputLayout> inputLayout; };
// Mesh.cpp #include "Mesh.h" Mesh::Mesh() : vertexCount(0), vertexBuffer(0), inputLayout(0) { } bool Mesh::InitBuffers(ID3D11Device* device, ID3DBlob* vsBuffer) { // 삼각형의 정점 데이터 Vertex vertices[] = { Vertex(0.0f, 0.5f, 0.0f), Vertex(0.5f, -0.5f, 0.0f), Vertex(-0.5f, -0.5f, 0.0f) }; // 정점의 개수 vertexCount = ARRAYSIZE(vertices); // 버퍼에 대한 설정 D3D11_BUFFER_DESC vertexBufferDesc; ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc)); vertexBufferDesc.ByteWidth = sizeof(vertices); // 생성할 정점 버퍼의 크기 vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; // 버퍼가 파이프라인에 바인딩되는 방법을 식별 vertexBufferDesc.CPUAccessFlags = 0; // CPU가 버퍼에 접근하는 방식을 결정하는 플래그들을 지정 vertexBufferDesc.MiscFlags = 0; // 기타 플래그 식별 vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; // 버퍼가 쓰이는 방식 // subresource를 초기화하기 위한 데이터를 지정 // 이 subresource의 데이터를 활용해 // 버퍼나 텍스처 등의 리소스들을 만들기 위해 사용된다. D3D11_SUBRESOURCE_DATA vertexBufferData; ZeroMemory(&vertexBufferData, sizeof(vertexBufferData)); vertexBufferData.pSysMem = vertices; // 초기화 데이터에 대한 포인터 // 버퍼 생성 HRESULT ret = device->CreateBuffer( &vertexBufferDesc, // D3D11_BUFFER_DESC에 대한 포인터 &vertexBufferData, // D3D11_SUBRESOURCE_DATA에 대한 포인터 vertexBuffer.GetAddressOf() // ID3D11Buffer에 대한 포인터 ); // 정점의 각 성분이 어떤 용도인지 D3D11_INPUT_ELEMENT_DESC layout[] = { {"POSITION", // 정점 쉐이더에서 쓰이는 정점 성분과 매핑되는 문자열 0, // 정점 성분과 매핑되는 문자열이 같은 것이 여러개 있는 경우에 사용하는 인덱스 DXGI_FORMAT_R32G32B32_FLOAT, // 성분의 자료 형식 0, // 이 성분의 자료를 가져올 입력 슬롯의 인덱스(0 ~ 15) 0, // 시작으로부터 해당 데이터까지의 오프셋(Byte) D3D11_INPUT_PER_VERTEX_DATA, // 입력 슬롯에 대한 입력 데이터 클래스를 식별(Vertex Data와 Instance Data가 있음) 0 // 이건 이해가 안되서 일단은 패스(단, D3D11_INPUT_PER_VERTEX_DATA를 쓰면 0으로 해야함) } }; // 입력 레이아웃 생성 ret = device->CreateInputLayout( layout, // D3D11_INPUT_ELEMENT_DESC에 대한 포인터 ARRAYSIZE(layout), // 정점 요소들의 개수 vsBuffer->GetBufferPointer(), // 컴파일된 셰이더에 대한 포인터 vsBuffer->GetBufferSize(), // 컴파일된 셰이더의 크기 inputLayout.GetAddressOf() // 생성된 입력 레이아웃에 대한 포인터 ); if (FAILED(ret)) { MessageBox(nullptr, L"입력 레이아웃 생성 실패", L"오류", 0); return false; } } void Mesh::RenderBuffers(ID3D11DeviceContext* deviceContext) { unsigned int stride = sizeof(Vertex); unsigned int offset = 0; // 정점 버퍼 배열을 입력 어셈블러 단계에 바인딩 // &stride : stride 값 배열에 대한 포인터 // &offset : 오프셋 값 배열에 대한 포인터 deviceContext->IASetVertexBuffers(0, 1, vertexBuffer.GetAddressOf(), &stride, &offset); // 입력 레이아웃을 입력 어셈블러 단계에 바인딩 deviceContext->IASetInputLayout(inputLayout.Get()); // 입력 데이터를 설명하는 데이터 순서에 대한 정보를 바인딩 deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->Draw(vertexCount, 0); }
이렇게 메쉬에 대한 클래스를 만들었으니
이제 이 메쉬를 처리해 줄 셰이더에 관한 클래스들을 만들어 보도록 하겠습니다.
셰이더는 화면에 출력될 픽셀의 위치와 색을 정하는 함수로
GPU의 프로그래밍이 가능한 렌더링 파이프라인을 프로그래밍하는데 쓰입니다.
일단은 렌더링 파이프라인에서 프로그래밍 가능한 영역들 중에서
VertexShader와 PixelShader에 관한 클래스를 만들어보도록 하겠습니다.
VertexShader는 주로 물체의 정점에 수학적인 연산을 해서 물체에 특별한 효과를 주는 데 쓰이며,
PixelShader는 렌더링 될 각각의 픽셀들의 색을 계산해서 최종적으로 픽셀이 어떻게 보일 지를 결정합니다.
그럼 이제 이 셰이더들에 관한 클래스들을 만들건데,
지금 만드는 클래스는 셰이더를 컴파일하고, 생성해서 파이프라인에 바인딩하는 역할을 합니다.
// Shader.h #pragma once #include <string> #include <d3d11.h> #include <D3DCompiler.h> #include <wrl.h> using Microsoft::WRL::ComPtr; // 셰이더를 컴파일, 생성하는 클래스들의 부모 클래스 class Shader { public: // 셰이더 경로, 셰이더 함수의 진입점, 셰이더의 버전 정보 초기화 Shader(std::wstring filename, std::string entry, std::string profile); // 셰이더 컴파일 virtual bool Compile(ID3D11Device* device) = 0; // 셰이더 생성 virtual bool Create(ID3D11Device* device) = 0; // 셰이더 바인딩 virtual void Bind(ID3D11DeviceContext* deviceContext) = 0; // getter std::wstring FileName() { return filename; } std::string Entry() { return entry; } std::string Profile() { return profile; } ID3DBlob* ShaderBuffer() { return shaderBuffer.Get(); } protected: std::wstring filename; // 셰이더 경로 std::string entry; // 셰이더 함수 진입점 std::string profile; // 셰이더 버전 정보 ComPtr<ID3DBlob> shaderBuffer; // 셰이더가 컴파일된 이진 데이터 };
// Shader.cpp #include "Shader.h" Shader::Shader(std::wstring filename, std::string entry, std::string profile) : filename(filename), entry(entry), profile(profile) { }
// VertexShader.h #pragma once #include "Shader.h" #include <wrl.h> using Microsoft::WRL::ComPtr; class VertexShader : public Shader { public: VertexShader(std::wstring filename); // 정점 셰이더 컴파일 bool Compile(ID3D11Device* device) override; // 정점 셰이더 생성 bool Create(ID3D11Device* device) override; // 정점 셰이더 바인딩 void Bind(ID3D11DeviceContext* deviceContext) override; private: ComPtr<ID3D11VertexShader> vs; };
// VertexShader.cpp #include "VertexShader.h" VertexShader::VertexShader(std::wstring filename) : Shader(filename, "VS", "vs_5_0"), vs(0) { } bool VertexShader::Compile(ID3D11Device* device) { // HLSL(High Level Shader Language) 코드를 지정된 대상에 대한 바이트 코드로 컴파일 HRESULT ret = D3DCompileFromFile( filename.c_str(), // 셰이더 파일의 경로 NULL, // 셰이더의 매크로를 정의하는 D3D_SHADER_MACRO의 선택적 배열 NULL, // ID3DInclude에 대한 포인터 entry.c_str(), // 셰이더 실행이 시작되는 진입점 함수의 이름 profile.c_str(), // 셰이더의 버전 정보 NULL, // 셰이더 컴파일 옵션의 조합 NULL, // 효과 컴파일 옵션의 조합 shaderBuffer.GetAddressOf(), // 컴파일된 코드에 액세스할 ID3DBlob에 대한 포인터 NULL // 컴파일러 오류 메시지에 액세스할 ID3DBlob에 대한 포인터 ); if (FAILED(ret)) { MessageBox(nullptr, L"정점 셰이더 컴파일 실패", L"오류", 0); return false; } return true; } bool VertexShader::Create(ID3D11Device* device) { // VertexShader 생성 HRESULT ret = device->CreateVertexShader( shaderBuffer.Get()->GetBufferPointer(), // 컴파일된 셰이더에 대한 포인터 shaderBuffer.Get()->GetBufferSize(), // 컴파일된 정점 셰이더의 크기 nullptr, // 클래스 연결 인터페이스에 대한 포인터 vs.GetAddressOf() // ID3D11VertexShader에 대한 포인터 ); if (FAILED(ret)) { MessageBox(nullptr, L"정점 셰이더 생성 실패", L"오류", 0); return false; } return true; } void VertexShader::Bind(ID3D11DeviceContext* deviceContext) { // 정점 셰이더를 설정 deviceContext->VSSetShader(vs.Get(), NULL, NULL); }
// PixelShader.h #pragma once #include "Shader.h" #include <wrl.h> using Microsoft::WRL::ComPtr; class PixelShader : public Shader { public: PixelShader(std::wstring filename); bool Compile(ID3D11Device* device) override; bool Create(ID3D11Device* device) override; void Bind(ID3D11DeviceContext* deviceContext) override; private: ComPtr<ID3D11PixelShader> ps; };
// PixelShader.cpp #include "PixelShader.h" PixelShader::PixelShader(std::wstring filename) : Shader(filename, "PS", "ps_5_0") { } bool PixelShader::Compile(ID3D11Device* device) { HRESULT ret = D3DCompileFromFile( filename.c_str(), NULL, NULL, entry.c_str(), profile.c_str(), NULL, NULL, shaderBuffer.GetAddressOf(), NULL ); if (FAILED(ret)) { MessageBox(nullptr, L"픽셀 셰이더 컴파일 실패", L"오류", NULL); return false; } return true; } bool PixelShader::Create(ID3D11Device* device) { HRESULT ret = device->CreatePixelShader( shaderBuffer.Get()->GetBufferPointer(), shaderBuffer.Get()->GetBufferSize(), nullptr, ps.GetAddressOf() ); if (FAILED(ret)) { MessageBox(nullptr, L"픽셀 셰이더 컴파일 실패", L"오류", 0); return false; } return true; } void PixelShader::Bind(ID3D11DeviceContext* deviceContext) { deviceContext->PSSetShader(ps.Get(), NULL, NULL); }
이렇게 해서 VertexShader와 PixelShader를 완성했습니다.
이제 이 두 개를 같이 컴파일, 생성, 바인딩할 수 있도록 클래스를 하나 더 만들어 보겠습니다.
클래스 이름은 BasicShader입니다.
// BasicShader.h #pragma once #include "VertexShader.h" #include "PixelShader.h" class BasicShader { public: static bool Compile(ID3D11Device* device); static bool Create(ID3D11Device* device); static void Bind(ID3D11DeviceContext* deviceContext); static ID3DBlob* ShaderBuffer(); private: static VertexShader vs; static PixelShader ps; };
// BasicShader.h #include "BasicShader.h" VertexShader BasicShader::vs = VertexShader(L"BasicVS.hlsl"); PixelShader BasicShader::ps = PixelShader(L"BasicPS.hlsl"); bool BasicShader::Compile(ID3D11Device* device) { if (!vs.Compile(device)) return false; if (!ps.Compile(device)) return false; return true; } bool BasicShader::Create(ID3D11Device* device) { if (!vs.Create(device)) return false; if (!ps.Create(device)) return false; return true; } void BasicShader::Bind(ID3D11DeviceContext* deviceContext) { vs.Bind(deviceContext); ps.Bind(deviceContext); } ID3DBlob* BasicShader::ShaderBuffer() { return vs.ShaderBuffer(); }
이렇게 해서 BasicShader 클래스를 완성했고,
이제 BasicShader를 통해서 컴파일, 생성, 바인딩되는 셰이더 파일을 만들어 보겠습니다.
셰이더 파일을 만들기 위해서는 HLSL(High Level Shader Language)이라는 언어를 사용해야 합니다.
HLSL의 문법은 C와 대체로 비슷합니다.
추가로 HLSL에는 Semantics라는 문법 또한 존재합니다.
이 Semantics는 변수 뒤에 :(콜론)과 Semantics 이름을 붙여서 사용하며,
Semantics는 매개 변수의 의도된 사용에 대한 정보를 전달하는 데 사용됩니다.
그럼 이제 VertexShader와 PixelShader의 HLSL을 작성하면서 기본적인 문법을 알아보겠습니다.
// BasicVS.hlsl // float4를 반환하고, float4를 매개 변수로 받는 함수 VS // 매개 변수 position은 (: POSITION)이 달렸기 때문에 정점의 위치를 의미한다. // 함수 끝에 있는 (: SV_POSITION)은 반환형에 대한 Semantics이며, // 여기서 SV_POSITION은 변환된 정점의 위치를 뜻한다. float4 VS(float4 position : POSITION) : SV_POSITION { return position; }
// BasicPS.hlsl // SV_TARGET은 렌더 타겟에 그려질 색상 값을 의미한다. float4 PS(float4 position : POSITION) : SV_TARGET { return float4(1.0f, 1.0f, 1.0f, 1.0f); }
이렇게 해서 삼각형을 띄우기 위한 모든 코드를 작성했습니다.
이제 작성한 함수들을 호출해 보도록 하겠습니다.
먼저 Engine.h에 다음을 추가합니다.
// Engine.h #include "Mesh.h" #include "BasicShader.h" class Engine { public: ... private: ... Mesh mesh; }
이어서 방금 생성한 Mesh와 셰이더 파일을 InitScene에서 컴파일 및 생성하고,
Draw 함수에서 바인딩해 주겠습니다.
// Engine.cpp ... void Engine::Draw() { float backgroundColor[4] = { 0, 0.75f, 0, 1 }; deviceContext.Get()->ClearRenderTargetView(renderTargetView.Get(), backgroundColor); // 쉐이더 바인딩 BasicShader::Bind(deviceContext.Get()); // 렌더링 mesh.RenderBuffers(deviceContext.Get()); swapChain.Get()->Present(1, 0); } bool Engine::InitScene() { // 쉐이더 컴파일 및 생성 // 메시 정점 데이터 초기화 if (!BasicShader::Compile(device.Get())) return false; if (!BasicShader::Create(device.Get())) return false; if (!mesh.InitBuffers(device.Get(), BasicShader::ShaderBuffer())); return true; } ...
이렇게 해서 호출하는 부분까지 완성했습니다.
하지만 여기서 실행을 하시면 "'main': entrypoint not found"라는 오류가 날 것입니다.
이건 파일 설정에서 진입점으로 설정된 함수가 없어서 발생하는 오류입니다.
따라서 아래와 같이 하시면 해결됩니다.
먼저 각 파일을 우클릭해서 속성에 들어갑니다.
여기서 '구성 속성 > HLSL 컴파일러 > 일반'으로 가서
진입점 이름을 해당 파일의 함수 이름으로 하면 됩니다.
이렇게 위의 오류는 해결되었지만,
"invalid vs_2_0 output semantic 'SV_TARGET'"이라는 새로운 오류가 하나 더 나오게 됩니다.
이 오류는 아마 BasicPS.hlsl 파일에서 나실 텐데 이것 또한 간단히 해결가능합니다.
먼저 BasicPS.hlsl의 속성으로 들어간 뒤,
'구성 속성 > HLSL 컴파일러 > 일반'으로 가서
셰이더 형식을 픽셀 셰이더(/ps)로 설정을 하면 됩니다.
이제 실행을 해보면 진짜로 삼각형이 뜨시는 걸 확인하실 수 있습니다.
다음 글에서는 이 삼각형에 텍스처를 입혀보도록 하겠습니다.
읽어주셔서 감사합니다.
'Game Development > DirectX' 카테고리의 다른 글
[DirectX] 6. 3D 모델 띄우기 (1) 2024.09.09 [DirectX] 5. 텍스처 입히기 (1) 2024.09.08 [DirectX] 3-4. Matrix4 클래스 제작 (0) 2024.06.21 [DirectX] 3-3. View 행렬, Projection 행렬 (0) 2024.06.19 [DirectX] 3-2. World 행렬 (0) 2024.06.17