int main()
{
const int width = 1280, height = 960;
const int canvasWidth = width / 80, canvasHeight = height / 80;
...
• width, height: 실제 윈도우가 갖게 될 해상도(픽셀 수).
• canvasWidth, canvasHeight: 우리가 직접 그릴 ‘캔버스’ 크기를 단순히 줄여서 사용할 수 있음. 위 예시 코드에서는 80으로 나누어, 예를 들어 16x12 정도의 작은 캔버스를 만듦(계산값에 따라 다름). 이 캔버스 크기만큼의 텍스처를 생성해서, 그 텍스처를 풀스크린 사각형에 맵핑하는 구조.
WNDCLASSEX wc = {
sizeof(WNDCLASSEX),
CS_CLASSDC,
WndProc,
0L,
0L,
GetModuleHandle(NULL),
NULL,
NULL,
NULL,
NULL,
L"HongLabGraphics",
NULL
};
RegisterClassEx(&wc);
RECT wr = { 0, 0, width, height };
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
HWND hwnd = CreateWindow(
wc.lpszClassName,
L"HongLabGraphics Example",
WS_OVERLAPPEDWINDOW,
100, 100,
wr.right - wr.left,
wr.bottom - wr.top,
NULL,
NULL,
wc.hInstance,
NULL
);
ShowWindow(hwnd, SW_SHOWDEFAULT);
UpdateWindow(hwnd);
• WNDCLASSEX를 통해 윈도우 클래스를 정의한 뒤, RegisterClassEx로 등록.
• CreateWindow를 통해 실제 윈도우 핸들(HWND)을 얻고, ShowWindow, UpdateWindow로 화면에 표시.
이런 Win32 API는 그래픽스보다는 Windows 창을 다루는 부분.
( 참고: 다양한 프레임워크/엔진에선 이 과정을 숨겨주므로 직접 볼 일은 많지 않을 수 있음. )
auto example = std::make_unique<Example>(hwnd, width, height, canvasWidth, canvasHeight);
• 여기서 Example 클래스 생성자 내부에서 D3D11 디바이스와 스왑체인 초기화를 모두 진행.
• unique_ptr은 C++ 스마트 포인터(자동으로 메모리를 해제).
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.DisplaySize = ImVec2(width, height);
ImGui::StyleColorsLight();
// Setup Platform/Renderer backends
ImGui_ImplDX11_Init(example->device, example->deviceContext);
ImGui_ImplWin32_Init(hwnd);
• ImGui는 UI를 빠르게 그릴 수 있도록 도와주는 라이브러리.
• DirectX 11과 Win32에 연동하기 위한 초기화 로직이 있음( _ImplDX11_Init, _ImplWin32_Init 등).
MSG msg = {};
while (WM_QUIT != msg.message)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// 실제 로직(업데이트 / 렌더링)
example->Update();
example->Render();
// 더블 버퍼링 스왑
example->swapChain->Present(1, 0);
}
}
• PeekMessage: Windows 메시지를 확인하고, 있으면 전달(DispatchMessage)하여 WinProc(=WndProc)으로 보냄.
• Update: 매 프레임마다 로직 갱신. 예제에서는 텍스처에 픽셀 데이터를 복사해 넣는 과정을 담당.
• Render: D3D 렌더링 호출(사각형에 텍스처를 그려서 화면에 표시).
• swapChain->Present(1, 0): 더블 버퍼를 스왑하여 화면에 최종 그림을 표시.
example->Clean();
DestroyWindow(hwnd);
UnregisterClass(wc.lpszClassName, wc.hInstance);
• Clean()에서 D3D 리소스를 모두 해제(Release).
• DestroyWindow, UnregisterClass는 Win32 창 리소스를 해제.
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))
return true;
switch (msg)
{
case WM_SIZE:
// TODO: 창 크기 변경 시 스왑체인 리사이즈
return 0;
case WM_SYSCOMMAND:
...
case WM_MOUSEMOVE:
...
case WM_LBUTTONUP:
...
case WM_RBUTTONUP:
...
case WM_KEYDOWN:
...
case WM_DESTROY:
::PostQuitMessage(0);
return 0;
}
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}
• 메시 종류에 따라 분기 처리
• WM_SIZE 같은 메시지는 창 크기가 변경될 때 발생. 일반적으로 D3D11에서는 스왑체인 Resize 작업을 해주어야 함.
• ImGui를 사용하면, ImGui_ImplWin32_WndProcHandler가 먼저 입력 이벤트(마우스, 키보드)를 받아 처리할 수 있도록 함.
ID3D11Device* device;
ID3D11DeviceContext* deviceContext;
IDXGISwapChain* swapChain;
D3D11_VIEWPORT viewport;
ID3D11RenderTargetView* renderTargetView;
ID3D11VertexShader* vertexShader;
ID3D11PixelShader* pixelShader;
ID3D11InputLayout* layout;
ID3D11Buffer* vertexBuffer = nullptr;
ID3D11Buffer* indexBuffer = nullptr;
ID3D11Texture2D* canvasTexture = nullptr;
ID3D11ShaderResourceView* canvasTextureView = nullptr;
ID3D11RenderTargetView* canvasRenderTargetView = nullptr;
ID3D11SamplerState* colorSampler;
UINT indexCount;
int canvasWidth, canvasHeight;
float backgroundColor[4] = { 0.8f, 0.8f, 0.8f, 1.0f };
• device: 그래픽 하드웨어와 리소스 생성 등의 작업을 할 수 있는 핵심 객체
• deviceContext: 실제 파이프라인 상태, 그리기 명령을 내리는 객체
• swapChain: 더블 버퍼링, 프런트/백 버퍼 교체 관리
• renderTargetView: 화면에 그릴 때, 어떤 버퍼(텍스처)에 그릴지를 지정
• vertexShader, pixelShader: 간단히 말해, 정점 처리(위치 변환)와 픽셀 처리(색상 결정) 로직이 담긴 프로그램
• layout: 정점 버퍼의 구조(예: (x, y, z, w), (u, v))를 정의
• vertexBuffer, indexBuffer: GPU 메모리에 올려둔 정점 데이터와 그 정점의 그리기 순서(인덱스)
• canvasTexture: 우리가 CPU에서 만든 픽셀 데이터를 복사해 넣을 수 있는 텍스처(동적(Dynamic) 사용)
• canvasTextureView: 셰이더에서 텍스처를 접근하기 위한 뷰
• canvasRenderTargetView: 텍스처 자체를 랜더링 대상(RenderTarget)으로도 쓸 수 있도록 하는 뷰
• colorSampler: 텍스처를 샘플링할 때 보간(필터링) 방법, UV 범위가 넘어섰을 때 보더나 랩(반복) 등을 어떻게 처리하는지 결정
• canvasWidth, canvasHeight: 캔버스 텍스처의 크기
• backgroundColor: (r, g, b, a) 형태의 클리어 색상
Example(HWND window, int width, int height, int canvasWidth, int canvasHeight)
{
Initialize(window, width, height, canvasWidth, canvasHeight);
}
• 객체 생성 시 즉시 Initialize를 통해 D3D 환경을 구축.
DXGI_SWAP_CHAIN_DESC swapChainDesc;
ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));
swapChainDesc.BufferDesc.Width = width;
swapChainDesc.BufferDesc.Height = height;
swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferCount = 2;
swapChainDesc.BufferDesc.RefreshRate.Numerator = 60;
swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.OutputWindow = window;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.Windowed = TRUE;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
UINT createDeviceFlags = 0;
// createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
const D3D_FEATURE_LEVEL featureLevelArray[1] = { D3D_FEATURE_LEVEL_11_0};
if (FAILED(D3D11CreateDeviceAndSwapChain(...)))
{
std::cout << "D3D11CreateDeviceAndSwapChain() error" << std::endl;
}
• DXGI_SWAP_CHAIN_DESC 구조체로 스왑체인의 특성을 설정:
• Width/Height: 백 버퍼 크기(창 크기와 동일)
• Format: 픽셀 형식(여기서는 RGBA 8비트)
• BufferCount = 2: 더블 버퍼링
• RefreshRate: 60fps로 맞출 수 있도록 60/1
• Windowed = TRUE: 창 모드
• SwapEffect = DXGI_SWAP_EFFECT_DISCARD: 가장 기본적인 교체 방식
• D3D11CreateDeviceAndSwapChain:
• device, deviceContext, swapChain 객체가 생성됨
• D3D_DRIVER_TYPE_HARDWARE를 사용하면 실제 GPU를 사용하게 됨.(소프트웨어 대신).
ID3D11Texture2D* pBackBuffer;
swapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
device->CreateRenderTargetView(pBackBuffer, NULL, &renderTargetView);
pBackBuffer->Release();
• GetBuffer(0): 스왑체인의 0번(백버퍼) 텍스처를 가져옴.
• CreateRenderTargetView: 그 텍스처를 “화면 렌더링 대상으로 쓸 수 있는 뷰”로 만듦.
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.Width = float(width);
viewport.Height = float(height);
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
deviceContext->RSSetViewports(1, &viewport);
• 화면에 그릴 좌표계를 설정. (0,0)은 왼쪽 상단, (width, height)는 오른쪽 하단, 깊이 범위(0~1).
void InitShaders()
{
ID3DBlob* vertexBlob = nullptr;
ID3DBlob* pixelBlob = nullptr;
ID3DBlob* errorBlob = nullptr;
D3DCompileFromFile(L"VS.hlsl", 0, 0, "main", "vs_5_0", 0, 0, &vertexBlob, &errorBlob);
D3DCompileFromFile(L"PS.hlsl", 0, 0, "main", "ps_5_0", 0, 0, &pixelBlob, &errorBlob);
device->CreateVertexShader(vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), NULL, &vertexShader);
device->CreatePixelShader(pixelBlob->GetBufferPointer(), pixelBlob->GetBufferSize(), NULL, &pixelShader);
// 인풋 레이아웃 정의
D3D11_INPUT_ELEMENT_DESC ied[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16, D3D11_INPUT_PER_VERTEX_DATA, 0},
};
device->CreateInputLayout(ied, 2, vertexBlob->GetBufferPointer(), vertexBlob->GetBufferSize(), &layout);
deviceContext->IASetInputLayout(layout);
}
• **HLSL(High Level Shading Language)**로 작성된 셰이더(VS.hlsl, PS.hlsl)를 컴파일하여, vertexBlob, pixelBlob을 얻어옴.
• CreateVertexShader, CreatePixelShader로 셰이더 객체 생성.
• 정점 버퍼 구조는 POSITION(float4)와 TEXCOORD(float2)로 구성됨.
• IASetInputLayout으로 정점 입력 구조를 GPU 파이프라인에 설정.
D3D11_SAMPLER_DESC sampDesc;
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
...
device->CreateSamplerState(&sampDesc, &colorSampler);
D3D11_TEXTURE2D_DESC textureDesc;
ZeroMemory(&textureDesc, sizeof(textureDesc));
textureDesc.Width = canvasWidth;
textureDesc.Height = canvasHeight;
textureDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
textureDesc.Usage = D3D11_USAGE_DYNAMIC;
textureDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
textureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
...
device->CreateTexture2D(&textureDesc, nullptr, &canvasTexture);
device->CreateShaderResourceView(canvasTexture, nullptr, &canvasTextureView);
• SamplerState: 텍스처 읽기 시 보간(POINT 필터) 및 범위 밖 주소(Clamp) 설정.
• Texture2D: RGBA 32bit float로 구성된 canvas 텍스처를 CPU가 수정할 수 있도록(D3D11_USAGE_DYNAMIC & CPUAccessFlags=WRITE).
• CreateShaderResourceView를 통해 이 텍스처를 픽셀 셰이더에서 접근 가능하도록 설정.
// Vertex
std::vector<Vertex> vertices = {
{{-1.0f, -1.0f, 0.0f, 1.0f}, {0.f,1.f}},
{{ 1.0f, -1.0f, 0.0f, 1.0f}, {1.f,1.f}},
{{ 1.0f, 1.0f, 0.0f, 1.0f}, {1.f,0.f}},
{{-1.0f, 1.0f, 0.0f, 1.0f}, {0.f,0.f}},
};
D3D11_BUFFER_DESC bufferDesc = {};
bufferDesc.Usage = D3D11_USAGE_DYNAMIC;
bufferDesc.ByteWidth = sizeof(Vertex)* vertices.size();
bufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
device->CreateBuffer(&bufferDesc, &vertexBufferData, &vertexBuffer);
// Index
std::vector<uint16_t> indices = {3,1,0, 2,1,3};
indexCount = indices.size();
...
device->CreateBuffer(&bufferDesc, &indexBufferData, &indexBuffer);
• 화면 전체를 덮는 정사각형(Quad)을 구성하는 정점 4개를 만듦.
• 정점0: (-1,-1)
• 정점1: ( 1,-1)
• 정점2: ( 1, 1)
• 정점3: (-1, 1)
• 텍스처 좌표(UV)는 (0,1) ~ (1,0) 사이로 지정하여, 이미지가 뒤집히지 않도록 세심하게 설정.
• 인덱스는 삼각형 2개로 정사각형을 그립니다. (총 6개 인덱스)
void Update()
{
std::vector<Vec4> pixels(canvasWidth * canvasHeight, Vec4{0.8f, 0.8f, 0.8f, 1.0f});
pixels[0 + canvasWidth * 0] = Vec4{ 1.0f, 0.0f, 0.0f, 1.0f };
pixels[1 + canvasWidth * 0] = Vec4{ 1.0f, 1.0f, 0.0f, 1.0f };
D3D11_MAPPED_SUBRESOURCE ms;
deviceContext->Map(canvasTexture, NULL, D3D11_MAP_WRITE_DISCARD, NULL, &ms);
memcpy(ms.pData, pixels.data(), pixels.size() * sizeof(Vec4));
deviceContext->Unmap(canvasTexture, NULL);
}
• (canvasWidth x canvasHeight) 크기의 CPU 메모리 배열을 만들어, 각각의 픽셀에 대해 RGBA 색상값을 지정(여기서는 회색(0.8,0.8,0.8)으로 초기화).
• 임의로 (0,0) 위치엔 빨간색, (1,0) 위치엔 노랑색을 넣어봄.
• deviceContext->Map을 통해 텍스처 리소스를 CPU가 쓸 수 있게 잠금(Map).
• memcpy로 CPU에서 만든 픽셀 데이터를 GPU 텍스처에 복사.
• Unmap으로 잠금 해제.
void Render()
{
float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
deviceContext->RSSetViewports(1, &viewport);
deviceContext->OMSetRenderTargets(1, &renderTargetView, nullptr);
deviceContext->ClearRenderTargetView(renderTargetView, clearColor);
deviceContext->VSSetShader(vertexShader, 0, 0);
deviceContext->PSSetShader(pixelShader, 0, 0);
UINT stride = sizeof(Vertex);
UINT offset = 0;
deviceContext->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);
deviceContext->IASetIndexBuffer(indexBuffer, DXGI_FORMAT_R16_UINT, 0);
deviceContext->PSSetSamplers(0, 1, &colorSampler);
deviceContext->PSSetShaderResources(0, 1, &canvasTextureView);
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->DrawIndexed(indexCount, 0, 0);
}
1. ClearRenderTargetView로 현재 렌더 타겟(백버퍼)을 검은색(0,0,0,1)으로 지움.
2. 정점 셰이더와 픽셀 셰이더를 바인딩.
3. 정점/인덱스 버퍼 연결 + PS 샘플러 & 텍스처 뷰 연결.
4. 삼각형 리스트로 인덱스의 개수(indexCount)만큼 Draw 호출.
• 결국은 Quad(사각형) 1개를 그립니다. 이 사각형은 canvasTexture를 텍스처로 갖는다.
void Clean()
{
if (layout) layout->Release();
if (vertexShader) vertexShader->Release();
if (pixelShader) pixelShader->Release();
if (vertexBuffer) vertexBuffer->Release();
...
// 모든 D3D 리소스 메모리 해제
}
• D3D11에서 생성한 객체는 COM(Reference Counting) 기반이므로, 사용 후에는 반드시 Release()로 참조를 줄여야 함.
• 이 코드가 없다면, 프로그램 종료 시 리소스 누수가 발생할 수 있음.
흐름 요약
1. main.cpp에서 Win32 윈도우를 생성
2. Example 객체 생성 시, D3D11 초기화, 셰이더 컴파일, 버퍼/텍스처 생성
3. 매 프레임 Update()에서 캔버스 텍스처 메모리를 CPU로부터 갱신
4. 매 프레임 Render()에서 검은 화면을 클리어 후, 사각형에 캔버스 텍스처를 그려서 출력
5. 메시 루프에서 Present()로 GPU가 그린 결과를 화면에 표시
6. 종료 시 Clean()으로 리소스 정리
추가 학습/연습 아이디어
1. 픽셀 데이터 변경
• Update()에서 특정 위치의 색을 바꾸는 대신, 간단한 패턴(예: 그라디언트, 체크무늬)을 만들기.
for(int y = 0; y < canvasHeight; y++) {
for(int x = 0; x < canvasWidth; x++) {
float r = float(x) / canvasWidth; // 0.0 ~ 1.0
float g = float(y) / canvasHeight; // 0.0 ~ 1.0
float b = 0.5f;
pixels[x + y * canvasWidth] = {r, g, b, 1.0f};
}
}
2. 마우스로 클릭해서 점찍기
• WM_LBUTTONUP에서 좌표를 얻어, Update()에 반영.
• “화면 해상도”와 “캔버스 해상도”가 다르므로, 클릭된 좌표를 canvasWidth, canvasHeight에 맞게 변환이 필요. 예를 들어,
// mouse_x, mouse_y는 화면(0~width, 0~height)
int canvas_x = (mouse_x * canvasWidth) / width;
int canvas_y = (mouse_y * canvasHeight) / height;