4 분 소요

들어가기 앞서

개인적으로 포스팅을 하는 글이며
기본적으로는 Honglab의 강의 글을 통해
공부하는 용도이다

시작 전에

사실 이거 말고도 이전 강의가 하나 더 있지만
Component Object Model (COM)에 관한 내용
(DirectX에서 유용하게 설명할 수 있으나
그래픽스와 직접적인 연관은 없기에 짧게 짚고 넘어간다)

  • COM ?
    마이크로소프트가 제공하는 일종의 인터페이스
    언어/컴파일러가 달라도 같은 인터페이스를 제공하는 Windows용 규약
  • WRL::ComPtr
    COM 객체용 스마트 포인터
    IUnknown을 상속받으면 해당 포인터로 관리 가능
    C++의 스마트 포인터 중 하나인 Shared_ptr과 유사하게 사용 가능
    (Device, Context, Buffer ,Texture, SwapChain 등이
    IUnknown을 상속받기에 ComPtr로 관리가 가능하다)
    (생포인터)

DirectX 초기화

전반적으로 DX 자체는 버전은 12까지 나왔으나
초기화 관련으로는 어느정도 틀이 잡혀있기에
관련 개념과 초기화 방식 위주로 집고 넘어갈 예정이다

(현재 코드 기준은 DX 11)

main.cpp

#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();
}

기본적인 프레임 워크로서
App 이라는 클래스를 생성 후
해당 Initialize와 Run을 호출시킨다
(사실상 main은 ‘프로그램 진입점’의 역할만 수행)

초기화의 성공 여부를 확인하고
프로그램을 실행시킨다

ExampleApp.h

class ExampleApp : public AppBase {
  public:
    ExampleApp(); // indexCount (렌더링시 vertex 렌더링 횟수 - 나중에는 뺌) 초기화 및 AppBase() 호출

    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;
};

이미 AppBase에서 선언된 내용을 override하여 주로 사용할 클래스
그 외에도 렌더링용 VS 나 PS 등을 가지고 있다
이번에는 ‘초기화’에 내용이 집중되어 있기에
일부 요소는 나중에 설명할 예정

(hlsl 파일이 존재하나 역시 이번엔 안다룬다)

AppBase.h

// 모든 예제들이 공통적으로 사용할 기능들을 가지고 있는
// 부모 클래스
class AppBase {
  public:
    AppBase(); // 여기서 해상도 세팅, m_mainWindow,m_screenViewport를 초기화
    virtual ~AppBase();

    float GetAspectRatio() const;

    int Run();

    virtual bool Initialize();
    virtual void UpdateGUI() = 0;
    virtual void Update(float dt) = 0;
    virtual void Render() = 0;

    virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    
    ...
    다양한 멤버 함수

  public:
    // 변수 이름 붙이는 규칙은 VS DX11/12 기본 템플릿을 따릅니다.
    // 다만 변수 이름을 줄이기 위해 d3d는 생략했습니다.
    // 예: m_d3dDevice -> m_device
    int m_screenWidth; // 렌더링할 최종 화면의 해상도
    int m_screenHeight;
    HWND m_mainWindow;

    ComPtr<ID3D11Device> m_device;
    ComPtr<ID3D11DeviceContext> m_context;
    ComPtr<ID3D11RenderTargetView> m_renderTargetView;
    ComPtr<IDXGISwapChain> m_swapChain;
    ComPtr<ID3D11RasterizerState> m_rasterizerSate;

    // Depth buffer 관련
    ComPtr<ID3D11Texture2D> m_depthStencilBuffer;
    ComPtr<ID3D11DepthStencilView> m_depthStencilView;
    ComPtr<ID3D11DepthStencilState> m_depthStencilState;

    D3D11_VIEWPORT m_screenViewport;

}

다양한 예제를 위한 기본 베이스가 될 클래스
Device, Context, 렌더 타겟과 스왑 체인,
DepthView와 State, 뷰포트 등을 변수로 가진다

멤버 함수를 다 적으면 100줄이 넘어가기에 스킵
(버퍼, Init, 입력 등에 관한 다양한 함수 존재)

AppBase 의 생성자

// 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;
}

생성자에서 g_appBase 라는 전역변수에 자신을 넣어줌
InitWindow()에서 콜백용 함수를 받을 때 사용하는 함수는
클래스의 멤버함수가 등록이 안되기에

임의로 전역 변수 + 함수 를 선언하여
클래스의 함수를 호출하도록 한다
(일반적으로 WndProc 라는 함수명을 사용)

  • 프로그램의 메시지 큐에 OS가 메시지를 전달해줌
    그렇기에 프로그램은 WM_KEYDOWN(키입력),
    WM_MOUSEMOVE(마우스 움직임) 같은
    입력처리를 직접 구현하지 않고 OS로부터
    메시지를 전달받는다
    (WndProc에서 호출하는 MsgProc 를 통해
    각 메시지(입력값)에 따른 처리를 할 예정)

ExampleApp::Initialize (1)

bool ExampleApp::Initialize() {

  // 부모 클래스에서 전반적인 초기화 진행
  if (!AppBase::Initialize())
      return false;

  ...
}

일단 AppBase가 전반적인 Device나 Context 등을 생성하며
Windows의 창을 띄우는 등의 역할을 해줄 예정

AppBase의 초기화를 먼저 보고 다시 오자

AppBase::Initialize()

bool AppBase::Initialize() {

    if (!InitMainWindow()) // Windows를 초기화하고 생성
        return false;

    if (!InitDirect3D()) // Direct 3D를 통해 만든 window에 어떻게 그림을 그릴지 세팅 초기화
        return false;

    if (!InitGUI())     // Imgui 초기화
        return false;

    return true;
}

초기화를 각각의 함수에 위임한다

AppBase::InitMainWindow()

bool AppBase::InitMainWindow() {
/ 구조체 제작
    WNDCLASSEX wc = {sizeof(WNDCLASSEX),
                     CS_CLASSDC,
                     WndProc, // 콜백함수 등록 (전역 함수)
                     0L,
                     0L,
                     GetModuleHandle(NULL),
                     NULL,
                     NULL,
                     NULL,
                     NULL,
                     L"HongLabGraphics", // lpszClassName, L-string (Wide Character)
                     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;
}

게임 엔진이나, 앱 등에서는 기본적으로 처리해주는 과정

  • WNDCLASSEX wc 를 생성하는 과정에서
    이전에 말하였던 WndProc 함수를 넣어준다
    (콜백함수 등록)

  • L”” : Wide Character로 이루어진 문자열
    (더 큰 크기의 문자 를 통해 더 많은 개수의 문자 표현 가능)

  • RegisterClassEx : 윈도우를 ‘등록’
    이후, CreateWindow 로 실제 윈도우를 만든다

  • 툴바 크기를 포함하지 않고 렌더링을 하기 위하여
    wr.right - wr.left, // 윈도우 가로 방향 해상도
    wr.bottom - wr.top, // 윈도우 세로 방향 해상도
    를 통해 윈도우 크기를 재계산
    (툴바의 크기를 화면 해상도에 포함시키지 않음)

  • ShowWindow : 실제 윈도우 창을 띄운다

댓글남기기