D3D 시작 [2/4], [3/4] - 예제 프레임워크, 초기화
- -
Luna의 DX11책은 절판됨, 코드가 현재 호환이 안됨(오래돼서)
Visual Studio 프로젝트 생성시 템플릿에 DirectX라고 입력하면 DirectX 11, DirectX 12 등의 템플릿이 나옴
이 템플릿 역시 공부하기에 좋은 코드는 아님.
예제의 구조
main.cpp
예제 코드에서 main 함수는 오직 초기화와 실행 기능만 있음.
나중에 실제 프로그램을 만들때는 win main을 사용할 가능성이 높음.
그러나 대부분 게임 프로그래밍을 하면 그건 엔진에서 처리해주는 부분이기 때문에 윈도우즈 프로그래밍에 집중할 것은 아님.
#include <iostream>
#include <memory>
#include <windows.h>
#include "ExampleApp.h"
using namespace std;
// main()은 앱을 초기화하고 실행시키는 기능만 합니다.
// 콘솔창이 있으면 디버깅에 편리합니다.
// 디버깅할 때 애매한 값들을 cout으로 출력해서 확인해보세요.
int main() {
hlab::ExampleApp exampleApp;
if (!exampleApp.Initialize()) {
cout << "Initialization failed." << endl;
return -1;
}
return exampleApp.Run();
}
exampleApp의 생성자만 호출하면 생성되는 거 아님?
-> 그러면 생성에 실패했는지 성공했는지 반환값이 없기 때문에 확인 불가. 따라서 Initialize라는 멤버함수를 호출하는 방식으로 구현했음.
생성 실패하는 경우가 있음? 그런 경우 왜 생각함?
-> 보통 피보나치 수열 구현하는 등의 엄청 간단한 코드는 이런 경우 생각안하고 코딩함. 그러나 외부 리소스를 가져다 쓰는 경우, 여기서는 그래픽 카드 리소스를 가져다 씀. 이 그래픽카드는 제조사도 다 다를것이고 경우의 수가 너무 많음. 따라서 뭘 하더라도 제대로 잘 동작했는지 확인하는것이 관습. 마찬가지로 네트워크랑 뭘 하는경우도 확인해야됨.
같은 원리로 exampleApp.Run()도 return할때 정상 실행됐는지 여부를 반환하게 되어있음. main()함수가 0을 반환하면 잘 끝났다는 의미.
ExampleApp
#pragma once
#include <algorithm>
#include <directxtk/SimpleMath.h>
#include <iostream>
#include <memory>
#include <vector>
#include "AppBase.h"
namespace hlab {
using DirectX::SimpleMath::Matrix;
using DirectX::SimpleMath::Vector3;
// 이 예제에서 사용하는 Vertex 정의
struct Vertex {
Vector3 position;
Vector3 color;
};
// 이 예제에서 ConstantBuffer로 보낼 데이터
struct ModelViewProjectionConstantBuffer {
Matrix model;
Matrix view;
Matrix projection;
};
class ExampleApp : public AppBase {
public:
ExampleApp();
virtual bool Initialize() override;
virtual void UpdateGUI() override;
virtual void Update(float dt) override;
virtual void Render() override;
protected:
ComPtr<ID3D11VertexShader> m_colorVertexShader;
ComPtr<ID3D11PixelShader> m_colorPixelShader;
ComPtr<ID3D11InputLayout> m_colorInputLayout;
ComPtr<ID3D11Buffer> m_vertexBuffer;
ComPtr<ID3D11Buffer> m_indexBuffer;
ComPtr<ID3D11Buffer> m_constantBuffer;
UINT m_indexCount;
ModelViewProjectionConstantBuffer m_constantBufferData;
bool m_usePerspectiveProjection = true;
};
} // namespace hlab
- ExampleApp 클래스는 AppBase라는 클래스를 상속해서 만들어진 클래스
- Initialize(), UpdateGUI(), Update(), Render() 함수를 override 했음.
- 나중에 코드를 짤때 Update, Render 등의 흔한 함수는 클래스에서 어떤 클래스를 상속받은 건지, override를 한건지 안한건지 주의깊게 보는 습관이 필요하다.
#include "ExampleApp.h"
#include <tuple>
#include <vector>
namespace hlab {
using namespace std;
auto MakeBox() {
vector<Vector3> positions;
vector<Vector3> colors;
vector<Vector3> normals;
const float scale = 1.0f;
// 윗면
positions.push_back(Vector3(-1.0f, 1.0f, -1.0f) * scale);
positions.push_back(Vector3(-1.0f, 1.0f, 1.0f) * scale);
positions.push_back(Vector3(1.0f, 1.0f, 1.0f) * scale);
positions.push_back(Vector3(1.0f, 1.0f, -1.0f) * scale);
colors.push_back(Vector3(1.0f, 0.0f, 0.0f));
colors.push_back(Vector3(1.0f, 0.0f, 0.0f));
colors.push_back(Vector3(1.0f, 0.0f, 0.0f));
colors.push_back(Vector3(1.0f, 0.0f, 0.0f));
normals.push_back(Vector3(0.0f, 1.0f, 0.0f));
normals.push_back(Vector3(0.0f, 1.0f, 0.0f));
normals.push_back(Vector3(0.0f, 1.0f, 0.0f));
normals.push_back(Vector3(0.0f, 1.0f, 0.0f));
// 아랫면
// 앞면
// 뒷면
// 왼쪽
// 오른쪽
vector<Vertex> vertices;
for (size_t i = 0; i < positions.size(); i++) {
Vertex v;
v.position = positions[i];
v.color = colors[i];
vertices.push_back(v);
}
vector<uint16_t> indices = {
0, 1, 2, 0, 2, 3, // 윗면
};
return tuple{vertices, indices};
}
ExampleApp::ExampleApp() : AppBase(), m_indexCount(0) {}
bool ExampleApp::Initialize() {
if (!AppBase::Initialize())
return false;
// Geometry 정의
auto [vertices, indices] = MakeBox();
// 버텍스 버퍼 만들기
AppBase::CreateVertexBuffer(vertices, m_vertexBuffer);
// 인덱스 버퍼 만들기
m_indexCount = UINT(indices.size());
AppBase::CreateIndexBuffer(indices, m_indexBuffer);
// ConstantBuffer 만들기
m_constantBufferData.model = Matrix();
m_constantBufferData.view = Matrix();
m_constantBufferData.projection = Matrix();
AppBase::CreateConstantBuffer(m_constantBufferData, m_constantBuffer);
// 쉐이더 만들기
// Input-layout objects describe how vertex buffer data is streamed into the
// IA(Input-Assembler) pipeline stage.
// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
// Input-Assembler Stage
// The purpose of the input-assembler stage is to read primitive data
// (points, lines and/or triangles) from user-filled buffers and assemble
// the data into primitives that will be used by the other pipeline stages.
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-input-assembler-stage
vector<D3D11_INPUT_ELEMENT_DESC> inputElements = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0},
};
AppBase::CreateVertexShaderAndInputLayout(L"ColorVertexShader.hlsl", inputElements,
m_colorVertexShader, m_colorInputLayout);
AppBase::CreatePixelShader(L"ColorPixelShader.hlsl", m_colorPixelShader);
return true;
}
void ExampleApp::Update(float dt) {
static float rot = 0.0f;
rot += dt;
// 모델의 변환
m_constantBufferData.model = Matrix::CreateScale(0.5f) * Matrix::CreateRotationY(rot) *
Matrix::CreateTranslation(Vector3(0.0f, -0.3f, 1.0f));
m_constantBufferData.model = m_constantBufferData.model.Transpose();
using namespace DirectX;
// 시점 변환
m_constantBufferData.view =
XMMatrixLookAtLH({0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f});
m_constantBufferData.view = m_constantBufferData.view.Transpose();
// 프로젝션
const float aspect = AppBase::GetAspectRatio();
if (m_usePerspectiveProjection) {
const float fovAngleY = 70.0f * XM_PI / 180.0f;
m_constantBufferData.projection =
XMMatrixPerspectiveFovLH(fovAngleY, aspect, 0.01f, 100.0f);
} else {
m_constantBufferData.projection =
XMMatrixOrthographicOffCenterLH(-aspect, aspect, -1.0f, 1.0f, 0.1f, 10.0f);
}
m_constantBufferData.projection = m_constantBufferData.projection.Transpose();
// Constant를 CPU에서 GPU로 복사
AppBase::UpdateBuffer(m_constantBufferData, m_constantBuffer);
}
void ExampleApp::Render() {
// IA: Input-Assembler stage
// VS: Vertex Shader
// PS: Pixel Shader
// RS: Rasterizer stage
// OM: Output-Merger stage
m_context->RSSetViewports(1, &m_screenViewport);
float clearColor[4] = {0.0f, 0.0f, 0.0f, 1.0f};
m_context->ClearRenderTargetView(m_renderTargetView.Get(), clearColor);
m_context->ClearDepthStencilView(m_depthStencilView.Get(),
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
// 비교: Depth Buffer를 사용하지 않는 경우
// m_context->OMSetRenderTargets(1, m_renderTargetView.GetAddressOf(), nullptr);
m_context->OMSetRenderTargets(1, m_renderTargetView.GetAddressOf(), m_depthStencilView.Get());
m_context->OMSetDepthStencilState(m_depthStencilState.Get(), 0);
// 어떤 쉐이더를 사용할지 설정
m_context->VSSetShader(m_colorVertexShader.Get(), 0, 0);
/* 경우에 따라서는 포인터의 배열을 넣어줄 수도 있습니다.
ID3D11Buffer *pptr[1] = {
m_constantBuffer.Get(),
};
m_context->VSSetConstantBuffers(0, 1, pptr); */
m_context->VSSetConstantBuffers(0, 1, m_constantBuffer.GetAddressOf());
m_context->PSSetShader(m_colorPixelShader.Get(), 0, 0);
m_context->RSSetState(m_rasterizerSate.Get());
// 버텍스/인덱스 버퍼 설정
UINT stride = sizeof(Vertex);
UINT offset = 0;
m_context->IASetInputLayout(m_colorInputLayout.Get());
m_context->IASetVertexBuffers(0, 1, m_vertexBuffer.GetAddressOf(), &stride, &offset);
m_context->IASetIndexBuffer(m_indexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_context->DrawIndexed(m_indexCount, 0, 0);
}
void ExampleApp::UpdateGUI() {
ImGui::Checkbox("usePerspectiveProjection", &m_usePerspectiveProjection);
}
} // namespace hlab
초기화가 진행되는 순서?
ExampleApp
ExampleApp::ExampleApp() : AppBase(), m_indexCount(0) {}
bool ExampleApp::Initialize() {
if (!AppBase::Initialize())
return false;
// Geometry 정의
auto [vertices, indices] = MakeBox();
// 버텍스 버퍼 만들기
AppBase::CreateVertexBuffer(vertices, m_vertexBuffer);
// 인덱스 버퍼 만들기
m_indexCount = UINT(indices.size());
AppBase::CreateIndexBuffer(indices, m_indexBuffer);
// ConstantBuffer 만들기
m_constantBufferData.model = Matrix();
m_constantBufferData.view = Matrix();
m_constantBufferData.projection = Matrix();
AppBase::CreateConstantBuffer(m_constantBufferData, m_constantBuffer);
// 쉐이더 만들기
// Input-layout objects describe how vertex buffer data is streamed into the
// IA(Input-Assembler) pipeline stage.
// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
// Input-Assembler Stage
// The purpose of the input-assembler stage is to read primitive data
// (points, lines and/or triangles) from user-filled buffers and assemble
// the data into primitives that will be used by the other pipeline stages.
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/d3d10-graphics-programming-guide-input-assembler-stage
vector<D3D11_INPUT_ELEMENT_DESC> inputElements = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0},
};
AppBase::CreateVertexShaderAndInputLayout(L"ColorVertexShader.hlsl", inputElements,
m_colorVertexShader, m_colorInputLayout);
AppBase::CreatePixelShader(L"ColorPixelShader.hlsl", m_colorPixelShader);
return true;
}
AppBase
// 참고: 헤더 include 순서
// https://google.github.io/styleguide/cppguide.html#Names_and_Order_of_Includes
#include "AppBase.h"
#include <dxgi.h> // DXGIFactory
#include <dxgi1_4.h> // DXGIFactory4
// imgui_impl_win32.cpp에 정의된 메시지 처리 함수에 대한 전방 선언
// VCPKG를 통해 IMGUI를 사용할 경우 빨간줄로 경고가 뜰 수 있음
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam);
namespace hlab {
using namespace std;
// RegisterClassEx()에서 멤버 함수를 직접 등록할 수가 없기 때문에
// 클래스의 멤버 함수에서 간접적으로 메시지를 처리할 수 있도록 도와줍니다.
AppBase *g_appBase = nullptr;
// RegisterClassEx()에서 실제로 등록될 콜백 함수
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// g_appBase를 이용해서 간접적으로 멤버 함수 호출
return g_appBase->MsgProc(hWnd, msg, wParam, lParam);
}
// 생성자
AppBase::AppBase()
: m_screenWidth(1280), m_screenHeight(960), m_mainWindow(0),
m_screenViewport(D3D11_VIEWPORT()) {
g_appBase = this;
}
...
bool AppBase::Initialize() {
if (!InitMainWindow())
return false;
if (!InitDirect3D())
return false;
if (!InitGUI())
return false;
return true;
}
...
bool AppBase::InitMainWindow() {
WNDCLASSEX wc = {sizeof(WNDCLASSEX),
CS_CLASSDC,
WndProc,
0L,
0L,
GetModuleHandle(NULL),
NULL,
NULL,
NULL,
NULL,
L"HongLabGraphics", // lpszClassName, L-string
NULL};
// The RegisterClass function has been superseded by the RegisterClassEx function.
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassa?redirectedfrom=MSDN
if (!RegisterClassEx(&wc)) {
cout << "RegisterClassEx() failed." << endl;
return false;
}
// 툴바까지 포함한 윈도우 전체 해상도가 아니라
// 우리가 실제로 그리는 해상도가 width x height가 되도록
// 윈도우를 만들 해상도를 다시 계산해서 CreateWindow()에서 사용
// 우리가 원하는 그림이 그려질 부분의 해상도
RECT wr = {0, 0, m_screenWidth, m_screenHeight};
// 필요한 윈도우 크기(해상도) 계산
// wr의 값이 바뀜
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, false);
// 윈도우를 만들때 위에서 계산한 wr 사용
m_mainWindow = CreateWindow(wc.lpszClassName, L"HongLabGraphics Example", WS_OVERLAPPEDWINDOW,
100, // 윈도우 좌측 상단의 x 좌표
100, // 윈도우 좌측 상단의 y 좌표
wr.right - wr.left, // 윈도우 가로 방향 해상도
wr.bottom - wr.top, // 윈도우 세로 방향 해상도
NULL, NULL, wc.hInstance, NULL);
if (!m_mainWindow) {
cout << "CreateWindow() failed." << endl;
return false;
}
ShowWindow(m_mainWindow, SW_SHOWDEFAULT);
UpdateWindow(m_mainWindow);
return true;
}
bool AppBase::InitDirect3D() {
// 이 예제는 Intel 내장 그래픽스 칩으로 실행을 확인하였습니다.
// (LG 그램, 17Z90n, Intel Iris Plus Graphics)
// 만약 그래픽스 카드 호환성 문제로 D3D11CreateDevice()가 실패하는 경우에는
// D3D_DRIVER_TYPE_HARDWARE 대신 D3D_DRIVER_TYPE_WARP 사용해보세요
// const D3D_DRIVER_TYPE driverType = D3D_DRIVER_TYPE_WARP;
const D3D_DRIVER_TYPE driverType = D3D_DRIVER_TYPE_HARDWARE;
// 여기서 생성하는 것들
// m_device, m_context, m_swapChain,
// m_renderTargetView, m_screenViewport, m_rasterizerSate
// m_device와 m_context 생성
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;
const D3D_FEATURE_LEVEL featureLevels[2] = {
D3D_FEATURE_LEVEL_11_0, // 더 높은 버전이 먼저 오도록 설정
D3D_FEATURE_LEVEL_9_3};
D3D_FEATURE_LEVEL featureLevel;
if (FAILED(D3D11CreateDevice(
nullptr, // Specify nullptr to use the default adapter.
driverType, // Create a device using the hardware graphics driver.
0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
createDeviceFlags, // Set debug and Direct2D compatibility flags.
featureLevels, // List of feature levels this app can support.
ARRAYSIZE(featureLevels), // Size of the list above.
D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Microsoft Store apps.
&device, // Returns the Direct3D device created.
&featureLevel, // Returns feature level of device created.
&context // Returns the device immediate context.
))) {
cout << "D3D11CreateDevice() failed." << endl;
return false;
}
/* 참고: 오류가 있을 경우 예외 발생 방법
// MS 예제
inline void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
// Set a breakpoint on this line to catch Win32 API errors.
throw Platform::Exception::CreateException(hr);
}
}
// Luna DX12 교재
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
*/
if (featureLevel != D3D_FEATURE_LEVEL_11_0) {
cout << "D3D Feature Level 11 unsupported." << endl;
return false;
}
// 참고: Immediate vs deferred context
// A deferred context is primarily used for multithreading and is not necessary for a
// single-threaded application.
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-devices-intro#deferred-context
// 4X MSAA 지원하는지 확인
UINT numQualityLevels;
device->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &numQualityLevels);
if (numQualityLevels <= 0) {
cout << "MSAA not supported." << endl;
}
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = m_screenWidth; // set the back buffer width
sd.BufferDesc.Height = m_screenHeight; // set the back buffer height
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // use 32-bit color
sd.BufferCount = 2; // Double-buffering
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // how swap chain is to be used
sd.OutputWindow = m_mainWindow; // the window to be used
sd.Windowed = TRUE; // windowed/full-screen mode
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // allow full-screen switching
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
if (numQualityLevels > 0) {
sd.SampleDesc.Count = 4; // how many multisamples
sd.SampleDesc.Quality = numQualityLevels - 1;
} else {
sd.SampleDesc.Count = 1; // how many multisamples
sd.SampleDesc.Quality = 0;
}
if (FAILED(device.As(&m_device))) {
cout << "device.AS() failed." << endl;
return false;
}
if (FAILED(context.As(&m_context))) {
cout << "context.As() failed." << endl;
return false;
}
// 참고: IDXGIFactory를 이용한 CreateSwapChain()
/*
ComPtr<IDXGIDevice3> dxgiDevice;
m_device.As(&dxgiDevice);
ComPtr<IDXGIAdapter> dxgiAdapter;
dxgiDevice->GetAdapter(&dxgiAdapter);
ComPtr<IDXGIFactory> dxgiFactory;
dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));
ComPtr<IDXGISwapChain> swapChain;
dxgiFactory->CreateSwapChain(m_device.Get(), &sd, &swapChain);
swapChain.As(&m_swapChain);
*/
// 참고: IDXGIFactory4를 이용한 CreateSwapChainForHwnd()
/*
ComPtr<IDXGIFactory4> dxgiFactory;
dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {0};
swapChainDesc.Width = lround(m_screenWidth); // Match the size of the window.
swapChainDesc.Height = lround(m_screenHeight);
swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // This is the most common swap chain format.
swapChainDesc.Stereo = false;
swapChainDesc.SampleDesc.Count = 1; // Don't use multi-sampling.
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 2; // Use double-buffering to minimize latency.
swapChainDesc.SwapEffect =
DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // All Microsoft Store apps must use this SwapEffect.
swapChainDesc.Flags = 0;
swapChainDesc.Scaling = DXGI_SCALING_NONE;
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
ComPtr<IDXGISwapChain1> swapChain;
dxgiFactory->CreateSwapChainForHwnd(m_device.Get(), m_mainWindow, &swapChainDesc, nullptr,
nullptr, swapChain.GetAddressOf());
*/
if (FAILED(D3D11CreateDeviceAndSwapChain(0, // Default adapter
driverType,
0, // No software device
createDeviceFlags, featureLevels, 1, D3D11_SDK_VERSION,
&sd, &m_swapChain, &m_device, &featureLevel,
&m_context))) {
cout << "D3D11CreateDeviceAndSwapChain() failed." << endl;
return false;
}
// CreateRenderTarget
ID3D11Texture2D *pBackBuffer;
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
if (pBackBuffer) {
m_device->CreateRenderTargetView(pBackBuffer, NULL, &m_renderTargetView);
pBackBuffer->Release();
} else {
cout << "CreateRenderTargetView() failed." << endl;
return false;
}
// Set the viewport
ZeroMemory(&m_screenViewport, sizeof(D3D11_VIEWPORT));
m_screenViewport.TopLeftX = 0;
m_screenViewport.TopLeftY = 0;
m_screenViewport.Width = float(m_screenWidth);
m_screenViewport.Height = float(m_screenHeight);
// m_screenViewport.Width = static_cast<float>(m_screenHeight);
m_screenViewport.MinDepth = 0.0f;
m_screenViewport.MaxDepth = 1.0f; // Note: important for depth buffering
m_context->RSSetViewports(1, &m_screenViewport);
// Create a rasterizer state
D3D11_RASTERIZER_DESC rastDesc;
ZeroMemory(&rastDesc, sizeof(D3D11_RASTERIZER_DESC)); // Need this
rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_SOLID;
// rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_WIREFRAME;
rastDesc.CullMode = D3D11_CULL_MODE::D3D11_CULL_NONE;
rastDesc.FrontCounterClockwise = false;
m_device->CreateRasterizerState(&rastDesc, &m_rasterizerSate);
// Create depth buffer
D3D11_TEXTURE2D_DESC depthStencilBufferDesc;
depthStencilBufferDesc.Width = m_screenWidth;
depthStencilBufferDesc.Height = m_screenHeight;
depthStencilBufferDesc.MipLevels = 1;
depthStencilBufferDesc.ArraySize = 1;
depthStencilBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
if (numQualityLevels > 0) {
depthStencilBufferDesc.SampleDesc.Count = 4; // how many multisamples
depthStencilBufferDesc.SampleDesc.Quality = numQualityLevels - 1;
} else {
depthStencilBufferDesc.SampleDesc.Count = 1; // how many multisamples
depthStencilBufferDesc.SampleDesc.Quality = 0;
}
depthStencilBufferDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilBufferDesc.CPUAccessFlags = 0;
depthStencilBufferDesc.MiscFlags = 0;
if (FAILED(m_device->CreateTexture2D(&depthStencilBufferDesc, 0,
m_depthStencilBuffer.GetAddressOf()))) {
cout << "CreateTexture2D() failed." << endl;
}
if (FAILED(
m_device->CreateDepthStencilView(m_depthStencilBuffer.Get(), 0, &m_depthStencilView))) {
cout << "CreateDepthStencilView() failed." << endl;
}
// Create depth stencil state
D3D11_DEPTH_STENCIL_DESC depthStencilDesc;
ZeroMemory(&depthStencilDesc, sizeof(D3D11_DEPTH_STENCIL_DESC));
depthStencilDesc.DepthEnable = true; // false
depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;
if (FAILED(m_device->CreateDepthStencilState(&depthStencilDesc,
m_depthStencilState.GetAddressOf()))) {
cout << "CreateDepthStencilState() failed." << endl;
}
return true;
}
bool AppBase::InitGUI() {
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
(void)io;
io.DisplaySize = ImVec2(float(m_screenWidth), float(m_screenHeight));
ImGui::StyleColorsLight();
// Setup Platform/Renderer backends
if (!ImGui_ImplDX11_Init(m_device.Get(), m_context.Get())) {
return false;
}
if (!ImGui_ImplWin32_Init(m_mainWindow)) {
return false;
}
return true;
}
...
- main함수에서 ExampleApp의 생성자 호출
- ExampleApp의 생성자는 AppBase를 상속받았는데, AppBase의 생성자를 호출하게 되어있음.
- AppBase의 생성자로 가 보면 스크린 해상도 초기화, 윈도우 프로그래밍에 사용되는 m_mainWindow 포인터도 초기화.
- 보면 생성자에 별 기능이 없는데 왜? -> Initialize라는 별도의 멤버함수에 초기화 부분을 분산시켜놨음. 잘 동작했는지 안했는지 확인을 일일히 하기 위해서. 이런 스타일의 코드는 C++에서 많이 사용하는 코딩 스타일임.
- g_appBase = this; 이 코드는 뭐임? -> AppBase* 형의 전역변수에 이 객체를 등록.
- 왠 전역변수? -> 윈도우 프로그래밍 스타일인데, 마우스 입력 키보드 입력 등을 받아올때, 이벤트들을 받아오기 위한 용도
- 윈도우 프로그래밍에서는 그런 이벤트가 발생하면 이런 함수를 불러달라라고 콜백 함수를 등록할 수 있음.
- 콜백? -> 전화해서 이러이러하면 다시 걸어줘요 하는것. 윈도우에서 마우스 입력등이 발생하면 불러달라고 콜백 함수를 등록할 수 있음.
- 그런데 멤버함수는 콜백 함수로 등록할 수 없음. 그래서 멤버함수가 아닌 일반 함수를 콜백함수로 등록시켜놓고, 대신에 간접적으로 멤버 함수를 호출하는 형식으로 구현.
- 이렇게 하면 원하는 콜백함수 기능 구현을 멤버함수에 넣을 수 있고, 이 클래스를 상속받은 자식 클래스에도 이 기능이 들어가게 됨.
- 이제 AppBase의 생성자가 끝났고, ExampleApp의 생성자로 돌아감.
- m_indexCount(0)으로 초기화 하는데 이건 렌더링할때 vertex 몇개를 렌더링 할지 갯수를 저장해주는 변수.(이건 여러 물체 그릴때는 없앨 코드)
- 이제 메인으로 돌아와서 EampleApp::Initialize()를 호출함.
- 공통적으로 초기화 해야되는 부분은 부모클래스인 AppBase에 구현해 놓았다. 따라서 Direct3D의 초기화 부분은 AppBase의 Initialize에 있음. 이제 AppBase의 Initialize로 이동.
- AppBase의 Initialize에서는 크게 세 가지 부분이 있음. 윈도우즈 프로그래밍 관점에서 윈도우를 만드는 부분(InitMainWindow), 그 윈도우에다가 그림을 어떻게 그릴지 초기화(InitDirect3D), GUI만들때 필요한 ImGui 초기화 부분(InitGUI)
- InitMainWindow: CreateWindow로 Window를 만들고 이 Window를 띄워준다.
- 여기서 주의깊게 보아야 할 부분은 WNDCLASSEX 라는 구조체를 하나 만들때, 여러 설정을 하는데 WndProc 함수를 주의깊게 보아야 함. WndProc(윈프로세스) 함수는 콜백함수를 등록해주는 것. 마우스 클릭같은 이벤트가 발생했을때, 어떤 콜백함수를 호출할지 여기서 설정하는 것.
- 그 다음으로 주목해야 할 부분은 문자열 앞에 L을 붙이는 것. wchar_t 즉, wide character type인데 16bit를 사용함. 더 다양한 문자를 표현하고 싶을때 사용함.
- 이렇게 생성된 wc 객체를 가지고 RegisterClassEx(&wc) 이렇게 호출한다.
- 내가 만들려고 하는 윈도우 클래스를 등록하는 과정임.
- 이렇게 등록된 걸 가지고 CreateWindow로 실제 윈도우를 만들 수 있음.
- 여기서 우리는 실제 rendering되는 부분의 크기(해상도)를 지정하고 싶은데, 위의 툴바 부분이나 이런것들은 제외하고 계산하고 싶음.
- 이를 위해 앞에 계산했던, m_screenWidth, m_screenHeight를 이용해서 전체 윈도우 크기를 다시 계산 다시 계산한 값으로 createWindow를 호출. (AdjustWindowRect 사용)
이제 Direct3D 초기화 부분
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;
const D3D_FEATURE_LEVEL featureLevels[2] = {
D3D_FEATURE_LEVEL_11_0, // 더 높은 버전이 먼저 오도록 설정
D3D_FEATURE_LEVEL_9_3};
D3D_FEATURE_LEVEL featureLevel;
if (FAILED(D3D11CreateDevice(
nullptr, // Specify nullptr to use the default adapter.
driverType, // Create a device using the hardware graphics driver.
0, // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
createDeviceFlags, // Set debug and Direct2D compatibility flags.
featureLevels, // List of feature levels this app can support.
ARRAYSIZE(featureLevels), // Size of the list above.
D3D11_SDK_VERSION, // Always set this to D3D11_SDK_VERSION for Microsoft Store apps.
&device, // Returns the Direct3D device created.
&featureLevel, // Returns feature level of device created.
&context // Returns the device immediate context.
))) {
cout << "D3D11CreateDevice() failed." << endl;
return false;
}
사용자가 어느 하드웨어 쓰는지 모름, 어느 버전의 디렉트x 지원하는지 모름. 따라서 높은 버전부터 시도.
if (featureLevel != D3D_FEATURE_LEVEL_11_0) {
cout << "D3D Feature Level 11 unsupported." << endl;
return false;
}
CreateDevice 실패하면 false 리턴.
https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-d3d11createdevice
참고: Immediate vs deferred context
A deferred context is primarily used for multithreading and is not necessary for a
single-threaded application.
https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-devices-intro#deferred-context
failed 가 났을때 어떤식으로 처리하는가?
// Microsoft 예제
inline void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
// Set a breakpoint on this line to catch Win32 API errors.
throw Platform::Exception::CreateException(hr);
}
}
// Luna DX12 교재 예제
#ifndef ThrowIfFailed
#define ThrowIfFailed(x) \
{ \
HRESULT hr__ = (x); \
std::wstring wfn = AnsiToWString(__FILE__); \
if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
}
#endif
Multisample Anti-Aliasing
https://en.wikipedia.org/wiki/Multisample_anti-aliasing
DirectX에서는 MSAA를 제공한다.
스왑체인 생성
스왑 체인이 뭐임?
프론트 버퍼(스크린 버퍼)가 실제 모니터 화면에 그림을 그리는 동안 그래픽스 API는 백 버퍼에 그림.
백 버퍼는 여러개일 수 있음
왜? 백 버퍼에 그림?
프론트 버퍼, 백 버퍼가 모니터에 데이터를 전송해야되고, 모니터가 그리는데에도 시간이 걸림.
1초에 60번 그리는 모니터라고 해 보자.
스크린 버퍼가 모니터에 그림을 그리는 작업이 1초에 60번 일어나야 함.
모니터를 볼 때 화면이 자연스럽게 이어지는 것 처럼 보이려면 버퍼에서 모니터로 보내는 작업이 끊임없이 빠르게 일어나야 함.
여러개의 백 버퍼에 미리 그려 놓고, 그려 놓은 것을 보내기만 해서 우리가 눈으로 보는 모니터에서는 끊김없이 부드럽게 이어지도록 하는 것.
그림에서는 버퍼를 3개 사용하고 있는데, 예전에는 버퍼를 2개만 사용했었어서 더블 버퍼링이라고도 불렀음.
요즘에는 트리플 버퍼링도 많이 사용.
https://en.wikipedia.org/wiki/Swap_chain
https://learn.microsoft.com/ko-kr/windows/win32/direct3d9/what-is-a-swap-chain-
DXGI_SWAP_CHAIN_DESC sd; // 어떤 스왑체인을 만들지 설명한다. describe의 DESC 구조체
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = m_screenWidth; // set the back buffer width
sd.BufferDesc.Height = m_screenHeight; // set the back buffer height
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // use 32-bit color
sd.BufferCount = 2; // Double-buffering
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // how swap chain is to be used
sd.OutputWindow = m_mainWindow; // the window to be used
sd.Windowed = TRUE; // windowed/full-screen mode
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // allow full-screen switching
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
if (numQualityLevels > 0) {
sd.SampleDesc.Count = 4; // how many multisamples
sd.SampleDesc.Quality = numQualityLevels - 1;
} else {
sd.SampleDesc.Count = 1; // how many multisamples
sd.SampleDesc.Quality = 0;
}
if (FAILED(device.As(&m_device))) {
cout << "device.AS() failed." << endl;
return false;
}
if (FAILED(context.As(&m_context))) {
cout << "context.As() failed." << endl;
return false;
}
// 참고: IDXGIFactory를 이용한 CreateSwapChain()
/*
ComPtr<IDXGIDevice3> dxgiDevice;
m_device.As(&dxgiDevice);
ComPtr<IDXGIAdapter> dxgiAdapter;
dxgiDevice->GetAdapter(&dxgiAdapter);
ComPtr<IDXGIFactory> dxgiFactory;
dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));
ComPtr<IDXGISwapChain> swapChain;
dxgiFactory->CreateSwapChain(m_device.Get(), &sd, &swapChain);
swapChain.As(&m_swapChain);
*/
// 참고: IDXGIFactory4를 이용한 CreateSwapChainForHwnd()
/*
ComPtr<IDXGIFactory4> dxgiFactory;
dxgiAdapter->GetParent(IID_PPV_ARGS(&dxgiFactory));
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {0};
swapChainDesc.Width = lround(m_screenWidth); // Match the size of the window.
swapChainDesc.Height = lround(m_screenHeight);
swapChainDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // This is the most common swap chain format.
swapChainDesc.Stereo = false;
swapChainDesc.SampleDesc.Count = 1; // Don't use multi-sampling.
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = 2; // Use double-buffering to minimize latency.
swapChainDesc.SwapEffect =
DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // All Microsoft Store apps must use this SwapEffect.
swapChainDesc.Flags = 0;
swapChainDesc.Scaling = DXGI_SCALING_NONE;
swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
ComPtr<IDXGISwapChain1> swapChain;
dxgiFactory->CreateSwapChainForHwnd(m_device.Get(), m_mainWindow, &swapChainDesc, nullptr,
nullptr, swapChain.GetAddressOf());
*/
if (FAILED(D3D11CreateDeviceAndSwapChain(0, // Default adapter
driverType,
0, // No software device
createDeviceFlags, featureLevels, 1, D3D11_SDK_VERSION,
&sd, &m_swapChain, &m_device, &featureLevel,
&m_context))) {
cout << "D3D11CreateDeviceAndSwapChain() failed." << endl;
return false;
}
UNORM?
-> Unsigned Normalized integer.
0~255 -> 0~1 로 normalize 한 형태.
CreateRenderTargetView
렌더링을 할때는 메모리에 렌더링 해야 한다.
GPU에 있는 메모리를 어떤 관점으로 보는가, 어떻게 사용하는가? -> 이것을 View라고 함.
// CreateRenderTarget
ID3D11Texture2D *pBackBuffer;
m_swapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
if (pBackBuffer) {
m_device->CreateRenderTargetView(pBackBuffer, NULL, &m_renderTargetView);
pBackBuffer->Release();
} else {
cout << "CreateRenderTargetView() failed." << endl;
return false;
}
나중에는 백 버퍼가 아닌 곳도 렌더 타겟 뷰로 사용하는 경우도 있다.
지금 위 코드에서는 스왑체인이 갖고 있는 기본적인 백 버퍼를 렌더타겟뷰로 사용하고 있음
Set the viewport
// Set the viewport
ZeroMemory(&m_screenViewport, sizeof(D3D11_VIEWPORT));
m_screenViewport.TopLeftX = 0;
m_screenViewport.TopLeftY = 0;
m_screenViewport.Width = float(m_screenWidth);
m_screenViewport.Height = float(m_screenHeight);
// m_screenViewport.Width = static_cast<float>(m_screenHeight);
m_screenViewport.MinDepth = 0.0f;
m_screenViewport.MaxDepth = 1.0f; // Note: important for depth buffering
m_context->RSSetViewports(1, &m_screenViewport);
RSSetViewports에서 RS?
-> Rasterization Stage 후에 그래픽스 파이프라인 공부하면서 자세히 설명
Create a rasterizer state
// Create a rasterizer state
D3D11_RASTERIZER_DESC rastDesc;
ZeroMemory(&rastDesc, sizeof(D3D11_RASTERIZER_DESC)); // Need this
rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_SOLID;
// rastDesc.FillMode = D3D11_FILL_MODE::D3D11_FILL_WIREFRAME;
rastDesc.CullMode = D3D11_CULL_MODE::D3D11_CULL_NONE;
rastDesc.FrontCounterClockwise = false;
m_device->CreateRasterizerState(&rastDesc, &m_rasterizerSate);
Create depth buffer And Create depth stencil state
// Create depth buffer
D3D11_TEXTURE2D_DESC depthStencilBufferDesc;
depthStencilBufferDesc.Width = m_screenWidth;
depthStencilBufferDesc.Height = m_screenHeight;
depthStencilBufferDesc.MipLevels = 1;
depthStencilBufferDesc.ArraySize = 1;
depthStencilBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
if (numQualityLevels > 0) {
depthStencilBufferDesc.SampleDesc.Count = 4; // how many multisamples
depthStencilBufferDesc.SampleDesc.Quality = numQualityLevels - 1;
} else {
depthStencilBufferDesc.SampleDesc.Count = 1; // how many multisamples
depthStencilBufferDesc.SampleDesc.Quality = 0;
}
depthStencilBufferDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilBufferDesc.CPUAccessFlags = 0;
depthStencilBufferDesc.MiscFlags = 0;
if (FAILED(m_device->CreateTexture2D(&depthStencilBufferDesc, 0,
m_depthStencilBuffer.GetAddressOf()))) {
cout << "CreateTexture2D() failed." << endl;
}
if (FAILED(
m_device->CreateDepthStencilView(m_depthStencilBuffer.Get(), 0, &m_depthStencilView))) {
cout << "CreateDepthStencilView() failed." << endl;
}
// Create depth stencil state
D3D11_DEPTH_STENCIL_DESC depthStencilDesc;
ZeroMemory(&depthStencilDesc, sizeof(D3D11_DEPTH_STENCIL_DESC));
depthStencilDesc.DepthEnable = true; // false
depthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
depthStencilDesc.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;
if (FAILED(m_device->CreateDepthStencilState(&depthStencilDesc,
m_depthStencilState.GetAddressOf()))) {
cout << "CreateDepthStencilState() failed." << endl;
}
DXGI_FORMAT_D24_UNORM_S8_UINT
D24_UNORM -> depth 를 저장하는데 24bit로 저장, 형식은 Unsigned norm
S8_UINT -> stencil 저장하는데는 8bit 사용, 형식은 Unsigned int
Stencil buffer가 뭔데? -> 나중에 설명
D3D11_USAGE -> 텍스처 메모리에 대해 CPU와 GPU가 접근을 할 수 있냐 없냐를 설정하는 부분
https://learn.microsoft.com/ko-kr/windows/win32/api/d3d11/ne-d3d11-d3d11_usage
참고 (COM 인터페이스에 대해)
getAddressOf와 As는 Windows의 COM (Component Object Model) 프로그래밍에서 사용되는 함수들로, 객체 포인터의 관리와 인터페이스 간의 형 변환을 위해 사용됩니다. COM은 Microsoft에서 개발한 소프트웨어 컴포넌트 기술로, 서로 다른 소프트웨어 구성 요소들이 통신할 수 있도록 합니다. 여기서 각각의 함수에 대해 설명하겠습니다:
getAddressOf:
getAddressOf는 주로 인터페이스 포인터의 주소를 얻기 위해 사용됩니다. COM 인터페이스를 사용할 때, 종종 인터페이스 포인터를 다른 함수에 인자로 전달해야 할 경우가 있습니다. 이 때 getAddressOf를 사용하여 인터페이스 포인터의 주소를 얻어서 전달할 수 있습니다.
예를 들어, QueryInterface와 같은 함수는 인터페이스 포인터의 주소를 인자로 받아, 해당 인터페이스를 다른 인터페이스로 변환할 때 사용됩니다.
As:
As 함수는 한 COM 인터페이스를 다른 타입의 인터페이스로 안전하게 변환하는 데 사용됩니다. 이 함수는 QueryInterface 호출을 내부적으로 사용하여 지정된 인터페이스 타입으로의 변환을 시도합니다.
예를 들어, 한 COM 객체가 IUnknown 인터페이스 타입을 가지고 있고, 이를 IDispatch 타입으로 변환하고자 할 때 As 함수를 사용할 수 있습니다. 이 함수는 요청된 인터페이스가 존재하지 않으면 nullptr을 반환합니다.
As 함수는 타입 안전성을 제공하며, 인터페이스 간 변환 시 타입 체크를 자동으로 수행합니다.
이러한 함수들은 COM 프로그래밍의 핵심적인 부분으로, 다양한 COM 기반의 Windows API와 상호 작용하는 데 필수적입니다. 객체 간의 인터페이스 변환과 객체의 메모리 주소 접근은 COM 기반 프로그래밍에서 흔히 발생하는 작업들이며, 이러한 함수들은 이런 작업을 보다 안전하고 효율적으로 수행할 수 있게 도와줍니다.
'게임 개발 > 그래픽스' 카테고리의 다른 글
D3D 시작 [1/4] - Direct3D 개요 (1) | 2023.11.25 |
---|---|
COM (Component Object Model) (0) | 2023.11.25 |
Row-Major order vs Column-Major order (1) | 2023.11.23 |
조명(SimpleMath) (0) | 2023.11.23 |
DirectXMath(SimpleMath) (1) | 2023.11.22 |
소중한 공감 감사합니다