graphics

기본개념

S0LL 2024. 12. 27. 22:12
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;

'graphics' 카테고리의 다른 글

Setting  (0) 2024.12.18