Graphics Pipeline

Shader Stage 들이 직접 코딩할 수 있는 부분이고, 나머지는 API 내부에서 처리해준다.

우리가 다룰 부분은 Vertex Shader와 Pixel Shader이다.

  • Vertex shader: 각 vertex에 적용할 수 있는 transformation이나 lighting 등을 수행
  • Pixel(fragment) shader : 각 pixel마다 색상을 계산하여 처리

 

 

 

 

Input-Assembler(IA) Stage

◇ CPU가 제공한 정보들을 primitive 형태로 모아준다.

  • 일반적으로 primitive는 삼각형을 의미한다. 
  • 관련해서 우리가 해줘야 할 일들은 다음과 같다.
    • 1) Create input buffers
    • 2) Create the input-layer object
    • 3) Bind objects to the input-assembler stage
    • 4) Specify the primitive type
    • 5) Call draw methods

 

 

 

IA Stage  - 1) Create input buffers

IA Stage에 전달해야 하는 버퍼 : Vertex buffer, Index buffer

 

Create a Vertex Buffer

 

1. Vertex 구조체를 만든다.

2. Vertex에 기하 정보(position)를 담은 메모리를 할당한다.

// header
struct SimpleVertex
{
	XMFLOAT3 Pos;
};

// global variables
ID3D11Buffer* g_pVertexBuffer = nullptr;

///////////////////////////////

SimpleVertex sVertices[] =
    {
        {XMFLOAT3 (0.0f, 0.5f, 0.5f)},
        {XMFLOAT3 (0.5f, -0.5f, 0.5f)},
        {XMFLOAT3 (-0.5f, -0.5f, 0.5f)},
    };

 

3. Buffer Description(버퍼 정보들)을 만들어준다. 

  • ByteWidth는 buffer의 크기, BindFlags는 어떤 buffer인지를 나타냄.

4. SUBRESOURCE_DATA를 만든다

  • 실제 데이터 정보에 대한 포인터를 전달해줌.
D3D11_BUFFER_DESC bd = {};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(SimpleVertex) * 3;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;


D3D11_SUBRESOURCE_DATA initData = {};
initData.pSysMem = sVertices;

 

5. CreateBuffer() 함수를 호출한다.

  • D3DDevice에 buffer를 만드는 것을 요청한다.
  • 함수 인자 값으로 앞서 만든 buffer description, subresource data, 그리고 buffer의 주소를 넣어준다.
hr = g_pd3dDevice->CreateBuffer(
    &bd,
    &initData,
    &g_pVertexBuffer
    );

if (FAILED(hr))
    return hr;

 

 

 Create a Index Buffer

같은 방식으로 Index Buffer도 만들어준다. 

 

1. Buffer를 만들고 index 정보를 저장한다.

2. Buffer description을 생성한다. 이때 flag를 Index buffer로 해야한다.

ID3D11Buffer* g_pIndexBuffer = nullptr;

///////////////////////////////

WORD sIndices[] = { 0, 1, 2 };

bd = {};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(WORD) * 3;
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = 0;

 

3. SUBRESOURCE_DATA를 만든다.

4. CreateBuffer() 함수를 호출한다.

initData = {};
initData.pSysMem = sIndices;

hr = g_pd3dDevice->CreateBuffer(
    &bd,
    &initData,
    &g_pIndexBuffer
);

if (FAILED(hr))
    return hr;

 

 

 

 

IA Stage  - 2)  Create the Input-Layout Object

  Input-Layout ObjectIA Stageinput에 대한 정보(앞에서 만든 buffer )를 전달해주는 역할을 한다.

  • Input data에 대한 설명을 해주는 input-element description을 만들어 준다.
  • 하나의 vertex는 position, normal, uv 등의 element 정보를 가질 수 있음. 하지만 지금은 position만 사용할 예정이기 때문에 이를 첫번째 파라미터로 명시해줬다.
  • 2번째 파라미터 : semantic index
  • 3번째 파라미터 : element 데이터의 format = 입력 데이터 타입에 따라 명시해주면 된다.
ID3D11InputLayout* g_pVertexLayout = nullptr;

///////////////////////////////

ID3DBlob* pVertexShaderBlob = nullptr;
D3D11_INPUT_ELEMENT_DESC layouts[] =
{
    {"POSITION",
    0,
    DXGI_FORMAT_R32G32B32_FLOAT,
    0,
    0,
    D3D11_INPUT_PER_VERTEX_DATA,
    0
    },
};

UINT uNumElements = ARRAYSIZE(layouts);

 

  • 4번째 파라미터 : input slot

IA Stage에는 여러 개의 vertex buffer가 들어갈 수 있다.(= 오브젝트가 여러 개인 경우)

각 n개의 vertex buffer를 하나의 슬롯으로 할당하여 IA의 input으로 들어간다. 따라서 Input-layout object에서는 몇 번째 slot을 다룰지에 대한 정보가 필요하다. 이게 4번째 파라미터 값이다.

  • 5번째 파라미터 : offset = 데이터를 어디서부터 읽을지 

이후 D3DDevice에세 InputLayout을 만들어달라고 요청한다.

  • CreateInputLayout() 파라미터 : layout / element 수 / vertex shader의 포인터 / vertex shader의 크기 / out 파라미터
  • Blob : shader 코드 데이터를 가리킨다고 생각하면 된다.
hr = g_pd3dDevice->CreateInputLayout(
    layouts,
    uNumElements,
    pVertexShaderBlob->GetBufferPointer(),
    pVertexShaderBlob->GetBufferSize(),
    &g_pVertexLayout
);

pVertexShaderBlob->Release();

if (FAILED(hr))
    return hr;

 

 

 

 

 

 

IA Stage  - 3)  Bind Objects to the IA Stage

  지금까지 만들어준 input buffer들과 input layout objectIA stagebinding 해준다.

  • Stride : vertex buffer에서 하나의 vertex 정보 크기 = SimpleVertex 구조체의 크기
  • Offset : vertex buffer에서 시작 지점
UINT uStride = sizeof(SimpleVertex);
UINT uOffset = 0;

g_pImmediateContext->IASetVertexBuffers(
    0, 		// slot
    1, 		// buffer의 개수
    &g_pVertexBuffer, 	// vertex buffer
    &uStride, 	    	// stride
    &uOffset	 	// offset
);

g_pImmediateContext->IASetIndexBuffer(
    g_pIndexBuffer,	 // index buffer
    DXGI_FORMAT_R16_UINT,	 // format of the index data
    0		 	 // offset
);

///////////////////////////

g_pImmediateContext->IASetInputLayout(g_pVertexLayout);

 

 

 

 

 

IA Stage  - 4)  Specify the Primitive Type

  IA stage에서 어떤 primitive type으로 assemble할 지에 대해 설정
앞서 언급한 것 처럼 삼각형 primitive로 설정해줬다.
g_pImmediateContext->IASetPrimitiveTopology(
    D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
);

 

 

 

 

IA Stage  - 5)  Call Draw Method

  • Binding이 된 후 draw 함수를 호출해서 그려 달라고 요청한다. 
  • Draw 요청을 하게 되면, DirectX가 그래픽스 파이프라인을 쭉 실행하여 화면에 출력해 준다.
  • Draw 함수 종류
    • Draw(): Index buffer 없이 vertex에 따라 draw
    • DrawIndexed(): index buffer를 이용하여 index 순서대로 vertex그려준다.

 

 

 

 

Vertex Shader(VS) Stage

vertextransformation, skinning, per-vertex lighting 과 같은 처리들을 해준다.

  • vertex 개수만큼 반복 실행된다. = VS의 input도 vertex 하나이고, output도 vertex 하나이다.
  • VS는 하나의 vertex를 입력으로 하고, 하나의 vertex를 출력하는 함수라고 생각하면 된다.
  • 그리고 앞으로 이 함수를 우리가 정의해 줄 예정이다.

 

 

 

Vertex Shader

  • Shader는 High-Level Shading Language(HLSL)이라는 언어로 작성된다. C++이 아닌 HLSL로 따로 작성하는 이유는 GPU에서 Matrix와 Vector 연산을 훨씬 빠르게 처리할 수 있기 때문이다.
  • VS에서 가장 중요한 일은 좌표계 사이를 transform 해주는 것이다.

 

  • input과 output은 float4 형태의 벡터(x,y,z,w)
  • POSITION : input parameter의 sementic
  • SV_POSITION : 함수의 반환값. 이때 SV는 System Value를 의미한다. = Clip-space의 position

 

 

 

 

Rasterizer Stage

3차원 좌표계에 있는 vertex들을 화면에 그리기 위한 2D pixel(raster image)로 변환해준다.

3D graphic을 2D pixel에 mapping 시키는 단계

 

Rasterization

  • 화면은 2차원 grid로 pixel로 되어있고 각 pixel들은 색깔을 가지고 있다. 이 pixel들을 칠해서 모니터에 화면이 만들어진다.
  • 아래 그림에서 왼쪽은 3개의 vertex로 구성된 삼각형이고 이를 rasterization 하면 오른쪽 그림처럼 된다.
  • GPU는 어떤 pixel이 삼각형이 포함되고, 칠해져야 하는지 여부를 계산하게 된다. 이후 칠해져야 하는 pixel들만 pixel shader 단계를 진행하게 된다.

 

 

 

 

Pixel Shader(PS) Stage

  최종 pixel의 색깔을 결정하는 단계 

  • Per-pixel 단위로 Lighting이나 post-processing 같은 처리를 해준다.
  • Input : pixel의 색상을 결정할 정보들
  • Output : 계산 후 결정된 최종 pixel 색상 정보 
float4 PS(float4 Pos : SV_POSITION) : SV_Target
{
    return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
  • 마찬가지로 input과 output은 float 4 타입이다.
  • Input의 semantic : SV_POSITION = vertex shader의 output
  • Output으로 color(RGBA)를 내보낸다. = SV_Target.

 

 

 

 

Creating the Shaders

D3DCompileFromFile 함수의 인자로 파일 이름이 들어가고, shader가 실행될 entry pointVS로 설정해준다. 이후 컴파일된 결과를 ppBlobOut으로 받아온다.
    DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;

#if defined (DEBUG) || defined(_DEBUG)
    dwShaderFlags |= D3DCOMPILE_DEBUG;
    dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
    
    ID3DBlob* pErrorBlob = nullptr;
    hr = D3DCompileFromFile(
    “VS.hlsl”, 	// FileName
    nullptr, 		// shader macros
    nullptr, 		// include files
    “VS”,		// Entry point
    “vs_5_0”,		// shader target
    dwShaderFlags, 	// flag1 
    0, 		// flag2
    ppBlobOut, 		// ID3DBlob out
    &pErrorBlob		// error blob out
    );

 

컴파일 한 뒤에는 Vertex Shader Object를 만들어준다. 이 과정 역시 Device가 요청하게 된다.

  • 파라미터 : 컴파일된 shader의 포인터 / shader의 size / Class link(nullptr) / shader를 가리키는 포인터( ID3D11VertexShaer*)
ID3D11VertexShader*  g_pVertexShader = nullptr;

///////////////////////////////

    hr = g_pd3dDevice->CreateVertexShader(
        pVertexShaderBlob->GetBufferPointer(), 
        pVertexShaderBlob->GetBufferSize(), 
        nullptr, 
        &g_pVertexShader);

    if (FAILED(hr))
    {
        pVertexShaderBlob->Release();
        return hr;
    }

 

Pixel Shader도 마찬가지로 만들어준다.

ID3D11PixelShader*  g_pPixelShader = nullptr;

///////////////////////////////

    hr = g_pd3dDevice->CreatePixelShader(
    	pPixelShaderBlob->GetBufferPointer(), 
    	pPixelShaderBlob->GetBufferSize(), 
    	nullptr, 
    	&g_pPixelShader);

    if (FAILED(hr))
    {
        pPixelShaderBlob->Release();
        return hr;
    }

 

 

 

 

 

Setting the Shaders

이제 만들어둔 shader를 pipeline에 binding 해준다.

g_pImmediateContext->VSSetShader(g_pVertexShader, nullptr, 0);
g_pImmediateContext->PSSetShader(g_pPixelShader, nullptr, 0);

 

 

 

 

 

다음 포스팅에서는 3D Space와 Transformation 과정에 대해 자세히 다룰 예정이다.

 

 

Reference KHU 강형엽 교수님 강의의 실습 수업을 수강하며 정리한 내용입니다.

 

 

이번 포스팅에서는 Window 초기화와 D3D 11 초기화에 대해 설명할 예정이다.

먼저 Window 초기화부터 코드와 함께 알아보겠다. 

 

 

 

 

InitWindow

Game.cpp에서 해당 함수에 대한 정의를 해줄 것이고, 아래 3단계로 진행된다.  1. Register the window class  2. Create window  3. Show window

 

 

 

 

InitWindow - Register window

// Library::Game.cpp
#include "Game.h"

LPCWSTR g_pszWindowClassName = L"GGPWindowClass";

HRESULT InitWindow(_In_ HINSTANCE hInstance, _In_ INT nCmdShow)
{
    WNDCLASSEX wcex =
    {
        .cbSize = sizeof(WNDCLASSEX),
        .style = CS_HREDRAW | CS_VREDRAW,
        .lpfnWndProc = WindowProc,
        .cbClsExtra = 0,
        .cbWndExtra = 0,
        .hInstance = hInstance,
        .hIcon = LoadIcon(hInstance,IDI_APPLICATION),
        .hCursor = LoadCursor(nullptr, IDC_ARROW),
        .hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1),
        .lpszMenuName = nullptr,
        .lpszClassName = g_pszWindowClassName,
        .hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION),
    };
}

 

좀 중요하게 볼 것들만 적자면

  • IpfnWndProc : 현재 window에서 발생한 이벤트를 처리할 window procedure (WindowProc 함수를 만들어줄 예정)
  • hInstance : 현재 실행하려는 application. Handle instance로, 식별 ID를 의미한다.
  • IpszClassName : 현재 window class의 이름이다. Window끼리 구분을 위해 사용하고 접두사 L은 const wchar_t*를 의미한다.

 

if (!RegisterClassEx(&wcex))
{
    DWORD dwError = GetLastError();

    MessageBox(
        nullptr,
        L"Call to RegisterClassEx failed!",
        L"Game Graphics Programming",
        NULL
    );

    if (dwError != ERROR_CLASS_ALREADY_EXISTS)
    {
        return HRESULT_FROM_WIN32(dwError);
    }

    return E_FAIL;
}

 

WNDCLASSEX를 RegisterClassEx를 통해 등록해준다. 추가로 문제가 있을 경우 오류 처리를 해주는 구문을 사용

 

 

 

 

InitWindow - Creating window

CreateWindow 함수를 통해서 등록한 window instance를 생성해준다. 

창에 대한 handle값을 g_hWnd로 저장한다. 

// Library::Game.cpp

// Global Variable
HWND g_hWnd = nullptr;
HINSTANCE g_hInstance = nullptr;
LPCWSTR g_pszWindowName = L"GGP02: Direct3D 11 Basics";


// InitWindow() 이어서
g_hInstance = hInstance;
RECT rc = { 0, 0, 800, 600 };
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

g_hWnd = CreateWindow(
    g_pszWindowClassName,
    g_pszWindowName,
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
    CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top,
    nullptr,
    nullptr,
    hInstance,
    nullptr
);

 

 

 

 

 

InitWindow - Show Window

마지막으로 윈도우 창을 보여주면 초기화 단계는 끝이 난다! winMain에서 InitWindow를 해주면 실행할 수 있다.

하지만 아직 WindowProc을 구현하지 않았기 때문에 아무것도 보여지지 않고 사용자 입력처리하는 부분도 없다. 

이제 사용자와 OS로부터 event를 어떤식으로 받고 응답하게 되는지 살펴보자. 

// Library::Game.cpp

ShowWindow(g_hWnd, nCmdShow);

return S_OK;

 

 

 

 

Window Messages

사용자와 OS로부터 받는 event는 message의 형태로 전달된다. 이때 Message는 단순한 숫자 형태로 정의된다.

  • 사용자 입력 : 마우스 클릭, 키보드 입력
  • OS 입력 : 새로운 하드웨어 연결, 저전력 모드 전환

이러한 event들은 프로그램이 돌아가는 중에 언제든 일어날 수 있고, 어떤 순서로 들어올 지 모른다. 이를 유연하게 해결하기 위해 window에서는 message-passing model을 사용한다.

 

 

 

 

 

Messages Loop

올바른 window에 message가 전달되도록 프로그램이 while문에서 message를 체크해주는 것

 

Application은 실행되는 동안 수천개의 메세지를 받는다. 하지만 여러 개의 window를 가진 프로그램일 수도 있기 때문에 올바른 window에 message가 전달되어야 한다. 이를 위해서는 프로그램이 실행되어 있는 동안, while문으로 무한 반복문을 돌리며 message를 체크해줘야 하고, 이를 message loop라고 부른다.

  • OS는 이 message들을 다루기 위한 message queue를 만들어 둔다. 이 queue 순서대로 while문에서 처리를 해준다.
  • 이때 while문에서  PeekMessage()(GetMessage()) 함수를 통해 queue에서 message를 받아온다.

GetMessage()

  • 일반적으로는 queue에서 message를 받아올 때 GetMessage()를 활용한다.
  • 이 함수는 queue가 비어 있을 경우, message가 올 때까지 무한히 기다리게 된다.
  • 특정 이벤트가 없으면 렌더링도 못하고 대기를 해버리기 때문에,, 게임에는 부적합하다. → 대신 PeekMessage() 사용

Message Loop 동작 예시

  • 사용자가 마우스 왼쪽 클릭
  • OS가 WM_LBUTTONDOWN message를 queue에 넣음
  • Message Loop에서 PeekMessage()함수가 호출됨
  • WM_LBUTTONDOWN message를 pull하고,  MSG structure에 message 정보를 넣음.
  • 이후, TranslationMessage()와 DispatchMessage() 함수가 실행됨
  • DispatchMessage() 함수에서 메시지에 해당하는 window의 procedure를 실행
  • Window procedure에서 해당 메시지를 처리
  • 다시 Message Loop로 돌아가 다음 메시지를 처리

 

 

 

 

Window Procedure

◇  OS에 의해 호출되는 콜백 함수로 message가 발생할 때 해당 이벤트를 처리해준다.

 

우리는 직접 WindowProc 함수를 만들어서 사용할 예정이다. 

LRESULT CALLBACK WindowProc(
  _In_ HWND hWnd, 
  _In_ UINT uMsg, 
  _In_ WPARAM wParam, 
  _In_ LPARAM lParam
);
  • 매개변수 : Message 대상이 되는 window의 handle / Message 내용 / Message 추가 정보(wParam, IParam)
  • return : LRESULT 값으로, procedure의 결과 값을 나타내는 정수 형태 값이다. 
LRESULT CALLBACK WindowProc(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_SIZE: // window resizing
        break;
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

 

보통 이런식으로 switch 문을 활용한다. Message 종류가 늘어나면 대응하는 함수를 추가해주면 된다.

OS가 보내는 모든 message에 대해 처리를 할 수 없기 때문에, (해당 window에 필요 없는 message도 포함된) 기본적으로 처리하지 않을 message들에 대해서는 DefaultWindowProc() 함수로 처리해준다.

 

 

 

 

Closing Window

◇  DestroyWindow() 함수를 호출해서 창을 닫아준다.

  • 사용자가 window를 끌 때 닫기 버튼을 누를 수도 있고 Alt+F4를 누를 수도 있다. 이 경우 모두 WM_CLOSE message로 전달된다.
  • 아래 처럼 종료 시에 MessageBox를 통해 확인하는 창을 띄워줄 수도 있다.
  • WM_DESTROY : Window가 destroy 되기 전에 맞는 Message. 여기서 PostQuitMessage()를 호출해 Message Loop를 멈추고, 할당한 메모리들을 해제해준다.
LRESULT CALLBACK WindowProc(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_CLOSE:
        if (MessageBox(hWnd,
            L"진짜 종료하시겠습니까?",
            L"Game Graphics Programming",
            MB_OKCANCEL) == IDOK)
        {
            DestroyWindow(hWnd);
        }
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
        
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}
  • 아래와 같은 종료 루프를 가진다.

 

 


 

지금까지 Window 초기화에 대해 코드와 함께 알아보았고, 여기서부터는 D3D 초기화에 대해 다룰 예정이다.

 

 

 

 

DirectX APIs

◇  Direct2D(D2D) / Direct3D (D3D)는 각각 2D/3D Graphics를 나타내기 위한 API이다.

  • Direct Write는 text layout을 담당한다.
  • DirecX Graphics Infrastructure(DXGI)는 low-level에서 rendering 관련된 일을 수행한다. D3D와 그래픽 드라이버 사이를 연결해준다. 
  • D3D는 하드웨어를 가속시킨다. 즉, CPU가 아닌 GPU를 통해 그래픽 연산을 훨씬 빠르게 처리할 수 있다. 

 

 

 

 

 

D3D Initialization

이제 D3D를 통해 만들어진 window에 Scene을 만들어 볼 것이다. 

초기화를 위해서는 Swap Chain / Device / Immediate Context 이 세가지를 만들어줘야한다. 

 

 

 

 

DirectX Device Resource

  • 앞서 DXGI를 통해 low-level graphics driver와 D3D를 연결한다고 설명했다.
  • 버퍼(buffer) : DXGI를 통해 가져오는 그려야 할 공간 = 렌더링하는 메모리 공간. GPU 메모리 상에 버퍼가 있고, 이를 DXGI를 통해 화면에 보여주게 된다.
  • 더블버퍼링(Double Buffering) : 화면상에 보이고 있는 버퍼를 'Front Buffer'라고 하고(수정할 일이 없기 때문에 Read-only), 보이지 않는 버퍼(그리고 있는 버퍼)를 'Back Buffer'라고 한다. Front가 보여지는 동안 미리 Back Buffer에서 새로운 화면을 그린다. 그리고 화면이 바뀔 때 Front와 Back Buffer를 swap(present)한다. 복사하는 것이 아니라 두 버퍼를 가리키는 포인터를 서로 맞바꾸는 것임. 
  • 렌더 타겟(Render Target) : 실제로 렌더링이 일어나고 있는 Back Buffer를 의미한다.

 

  • Swap Chain : Flip을 하기 위해 순차적으로 쌓이는 DXGI에 있는 버퍼들의 집합을 의미한다. D3D에서는 IDXGISwapChain이라는 인터페이스를 사용한다.
  • 화면에 draw를 하기 위해 다음 과정을 수행하게 된다. 
    1. application을 위한 window만들기 
    2. D3D 사용하기 위한 interface 만들기.
    3. 생성된 windowrendering하기 위한 swap chain을 만들기.
    4. Render target을 만들고, pixel을 채운다.
    5. Swap chainpresent 한다. (2개의 buffer를 교환하는 것

 

 

 

 

D3D 11 Device & Context

D3D를 쓰기 위해서는 GPU를 위한 interface를 생성해야 한다. 이때  ID3D11DeviceID3D11DeviceContext가 생성된다.

 

  • ID3D11Device : 자원 관리 (할당/해제)
    • 픽셀에 draw하기 위한 자원을 할당하고 구성하는 역할
    • 필요할 때 호출되는 method들로 이루어져 있다.
  • ID3D11DeviceContext : 기능 관리 (렌더링 과정)
    • 버퍼, 뷰, 기타 자원 로드, drawing과 같은 매 프레임 호출되는 method 들이 포함되어 있다.

InitDevice에서 D3D Initialization을 해준다. 아래와 같은 다양한 초기화를 담당한다. (자세한 코드는 생략)

HRESULT InitDevice()
{
    // Create D3D 11 device & context
    // Obtain DXGI Factory from device
    // Create swap chain
    // Create render target view
    // Setup the viewport
}

 

초기화 과정에서 D3D11CreateDevice() 함수를 통해 devicedevice context을 모두 만들어준다.
HRESULT D3D11CreateDevice(
    [in, optional]  IDXGIAdapter*           pAdapter,
                    D3D_DRIVER_TYPE         DriverType,
                    HMODULE                 Software,
                    UINT                    Flags,
    [in, optional]  const D3D_FEATURE_LEVEL*pFeatureLevels,
                    UINT                    FeatureLevels,
                    UINT                    SDKVersion,
    [out, optional] ID3D11Device**          ppDevice,
    [out, optional] D3D_FEATURE_LEVEL*      pFeatureLevel,
    [out, optional] ID3D11DeviceContext**   ppImmediateContext
);

 

 

 

 

 

 

 

Reference KHU 강형엽 교수님 강의의 실습 수업을 수강하며 정리한 내용입니다.

 

목표 : 3D 렌더링 엔진 만들기

  • Windlow Initialization / D3D Initialization
  • Support System
  • Cameras / Renderer
  • Models
  • Materials / Lights
  • Etc…

 

 

 

폴더 구조

  • Library Project : 일반적인 렌더링 엔진 구조, 게임에서 공통되게 사용하는 기능들 구현
  • Game Project : 어플리케이션에 따라 구체적인 기능들 구현

 

 

 

 

Window란? 

  • Application window 또는 Main window라고 불란다.
  • Frame 구성은 다음과 같다.
    • Title bar / Minimize, Maximize button / Other standard UI
  • Frame 안에 영역을 Client Area 라고 한다. 
  • OK 버튼이나 EditBox들도 Window의 한 종류이다. 

 

 

 

 

 

Parent Windows & Owner Windows

  • UI control window는 application window의 자식 관계이다.
  • Parent window는 Child window에게 좌표계를 제공하기 때문에 child는 parent의 좌표계 영역 밖에서 나타나지 않는다.
  • 새로운 window 알림 창이 나타나는 경우를 dialog라고 한다. 

 

 

 

Window Handles

  • Windows들은 object지만 C++의 class는 아니다.
  • 특정 Window를 나타내기 위해서 handle이라는 변수를 사용한다. 이때 Handle이란 OS가 object를 구분하기 위한 ID 숫자값이다. (포인터가 아니라 단순한 숫자임)
  • Window handle의 자료형은 HWND를 사용한다.

 

 

 

Screen & Window Coordinates

  • 컴퓨터 상에서 좌표계는 pixel 값에 따른다.
  • 기준에 따라 다음과 같이 3개의 좌표계가 존재한다.
    • Screen/Window/Client 좌표계
  • 모든 좌표계는 좌상단 점을 원점으로 한다.

 

 

 

 

WinMain

◇  모든 window 프로그램은 WinMain이라는 entry-point가 있다. 

  • C/C++에서 in main 함수와 같은 역할이다.

  • hInstance : handle to an instance라고 불리며 OS가 이 값을 이용해서 해당 프로그램이 메모리에 로드될 때 EXE파일의 ID를 구분하게 된다. Handle이기 때문에 포인터가 아닌 정수값이다. 
  • hPrevInstance : 지금은 중요하지 않은 값. 항상 0으로 사용할 것이다.
  • pCmdLind : command-lind argument를 Unicode로 인코딩된 string으로 가진다. 
  • pCmdShow : min applicaiton이 minimize/maximize/일반 중 어떤식으로 보여줄 지 나타내는 flag값
  • Return 값 : int (C/C++과 동일하게 프로그램의 상태를 나타내는 값이다.)

 

 

 

Game::Main.cpp - 전처리

UNICODE를 사용해 빌드하기 위해 전처리로 매크로 정의를 해준다.

그리고 Windows desktop 프로그램은 <windows.h>를 include 해야한다. 

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>

 

 

 

 

Game::Main.cpp - wWinMain

기본적으로 사용되는 엔진구조는 아래와 같다. 

  • Initialization : 어플리케이션이 시작하면서 초기화한다. 필요한 에셋과 데이터를 로드하고 메모리 할당과 같은 초기화 작업을 진행한다.
  • Game Loop : 사용자의 입력의 받아 게임 로직을 수행하고, 이를 렌더링한다.
  • Destruction : 게임이 종료되면 메모리 해제와 같은 Destroy 과정을 거치며 종료한다. 
INT WINAPI wWinMain(
    _In_		HINSTANCE hInstance,
    _Int_opt 	HINSTANCE hPrevInstance,
    _In_		LPWSTR IpCmdLine,
    _In_		INT nCmdShow)
{
	// Initialization
    
    while(! /* is game terminated condition set? */)
    {
    	// Handle Input
        // Update (Game Logic)
        // Draw
    }
    
    // Destroy
    
    return 0;
}

 

wWinMain의 매개변수도 간단히 설명하자면,,

  • _In_ : 호출된 함수에 전달되고, 읽기 전용으로 취급
  • _Out_ : 호출된 함수가 쓸 공간만 제공. 호출된 함수는 해당 공간에 데이터를 쓴다..
  • _In_opt_ : _In_과 같으며, 매개변수가 선택 사항임을 나타낸다. 

 

 

 

 

다음에는 이어서 Window 초기화와 D3D 초기화에 대해 알아보겠다.

 

 

Reference KHU 강형엽 교수님 강의의 실습 수업을 수강하며 정리한 내용입니다.

 

Direct3D 12 graphics Pipeline

이번 포스팅에는 마지막 단계인 Output Merger를 다룰 예정이다. 여러 요소들을 고려해서 최종 렌더링될 pixel의 섹상을 만들어주는 역할을 한다. 해당 픽셀이 실제 화면에 그려질지, 뒤에 가려지는지, 색을 섞여야하는지 등을 판단하게 된다.

 

 

 

 

Output-Merger(OM) Stage

◇  Pixel Shader의 output인 pixel은 Depth-Stencil Test, Color Blending의 과정을 거친다.

  • Depth-Stencil Test : Z-buffer를 활용하여 픽셀이 가려질 경우에 버린다.
  • Color Blending : 반투명한 물체나 파티클에서 기존 색과 Blending하는 과정이 필요하다

 

 

 

Pipeline State

◇   GPU가 '이 데이터를 어떻게 그릴지' 결정하는 하드웨어 설정들의 집합

  • Rasterizer State : Cull Mode(Black-face culling), Fill Mode(Wireframe/Solid), Viewport 설정
  • Blend State : Alpha Blending, Additive Blending(파티클) 등
  • Depth-Stencil State : 그릴지 말지 판단 - Depth Test, Stencil Test
  • Primitive Topology : 어떤 형태로 그릴 지 - Triangle, Line 등
  • Shader State : 어떤 셰이더를 쓸 지 - Vertex Shader, Pixel Shader

◇  Pipeline State Object (PSO) : Pipeline State를 하나로 묶어서 미리 생성한 객체

  • Direct3D 12에서 대부분의 그래픽 파이프라인 상태는 PSO를 사용하여 설정된다.
  • 초기화 시 일반적으로 여러 개의 PSO가 미리 생성되고, 이후 렌더링 시에는 상황에 맞는 PSO로 교체한다.
  • ex) PSO 1 : 기본 오브젝트(Depth Test On, Blending Off), PSO 2 : 투명 오브젝트(Depth Test ON, Blending ON)

 

 

 

 

 

Render Target

◇   GPU가 “그릴 대상”으로 사용하는 버퍼(텍스처)

  • 렌더링할 때 back buffer에 렌더링하여 바로 화면에 표시하는 대신, temporary intermediate buffer에 렌더링하여 추가적인 작업이나 후처리 효과를 적용할 수 있다. 

◇   Frame Buffer

  • 색(Color) 정보를 저장하는 메모리 영역 = RAM에 저장됨 

◇  Double Buffering

  • Front Buffer : 현재 화면에 보여지고 있는 버퍼
  • Back Buffer : 지금 그리고 있는 버퍼
  • Back Buffer에 미리 렌더링 → 작업 끝나면 → Front Buffer와 교체(Swap)
  • 이 과정을 통해서 화면 깜빡임 문제를 방지할 수 있다. back buffer 없이 바로 화면에 그린다면 중간 상태가 보여지면서 깜빡이는 문제가 발생한다.

 

 

Depth-Stencil Testing

 

◇  Depth-Stencil Buffer를 활용해서 픽셀을 그릴지 말지 결정한다.

  • depth data : 어떤 픽셀들이 camera에 가장 가까운지
  • stencil data : 어느 pixel을 update할 것인지

 

 

 

 

Depth Test

◇  Depth Buffer를 이용해 어떤 픽셀이 보일지 결정 = Z-buffering

  • 1) Depth 값 계산 후 Clamp  ( z = min(MaxDepth, max(MinDepth, z)) 활용 )  = Viewport 범위 [0~1] 또는 설정 범위로 제한하는 것
  • 2) Depth Buffer와 비교
    • pixel depth < stored depth : 픽셀이 업데이트 되어야 하는 군 → Color Buffer, Depth Buffer 업뎃
    • pixel depth > stored depth : 안보이는 픽셀 이므로 버림

 

◇  Z-buffering은 처리되는 순서와 상관없이 동일한 결과가 나온다.

 

 

 

 

 

Stencil Test

◇   Stencil Buffer 값이 1인 pixel만 Back Buffer로 채운다.

◇   Tansition effect 적용

 

◇  UI에 가려지는 부분 버리기 = fragment 수를 줄여서 성능을 최적화할 수 있다.

 

 

 

 

Color Blending

◇  새로 그려질 픽셀(c_src)과 기존 화면 색(c_dst)을 섞어서 최종 색(c)을 만드는 과정

  • 기본 렌더링은 Depth Test 통과 시에 덮어쓰게 되지만 반투명한 물체가 있는 경우 뒤의 오브젝트와 Blending이 필요하다. 
  • Output value가 렌더 타겟에 찍히기 전에 Pixel Shader의 Output에 Blending Operation이 동작한다.
  • 블렌딩을 위해서는 불투명한 물체와 투명한 물체를 구분하고, z값이 높은 순으로(back-to-front) 처리하게 된다.

 

 

 

 

 

 

이렇게 Rendering Piepline 포스팅을 마무리하겠습니다--

 

 

 

Reference KHU 강형엽 교수님 강의를 수강하며 정리한 내용입니다.

'그래픽스' 카테고리의 다른 글

[DirectX12] 4. Pixel Shader - Lighting  (0) 2026.03.13
[DirectX12] 3. Pixel Shader - Texturing  (0) 2026.03.13
[DirectX12] 2. Rasterizer  (1) 2026.03.13
[DirectX12] 1. Input Assembler, Vertex Shader  (0) 2026.03.12

💡 GitFront 사용

https://gitfront.io/

 

💡 How to ?

1. Sign Up

생략해도 되지만, 하지 않을 경우 생성한 Link에 7일 유효기간이 생긴다.

 

2. Add Repository

로그인 하면 아주 심플한 화면이 나온다.

  • Add Repository 클릭
  • Repository URL에 내 GitHub url 주소를 넣어주자.

 

3. Add → Convert to SSH

  • Add 버튼 클릭
  • Convert to SSH 클릭
  • 차례대로 클릭하면 요 화면이 나온다.
  • Copy 해준다.

 

4. Add Key in GitHub

이제 다시 깃허브로 가준다.

  • 본인 프로젝트 -> Settings -> Deploy keys
  • Title 마음대로 하고, 내용에 아까 복사한 Key를 넣어준다.
  • 그리고 Add Key

 

5. Build in GitFront

다시 GitFront로 가서

  • Build 클릭
  • View 클릭하면 프로젝트가 보인다.

 

    • View를 눌러서 프로젝트가 보이면, 오른쪽 상단에 Clone 버튼이 있다.
    • Clone을 클릭하면 링크가 나온다. 이제 이 링크를 공유하면 프로젝트를 볼 수 있다.
    • ❗ 단, 링크 끝에 .git을 지우고 공유해야 한다❗




Reference

https://www.youtube.com/watch?v=IejuI40oZ5E

 

 

Unity WebGL 환경에서는 일반적인 모바일이나 PC 환경보다 성능 제약이 훨씬 크다. 브라우저 위에서 동작하는 구조 특성상 메모리, CPU, GPU 자원이 제한적이며, 특히 고정된 힙 메모리와 싱글 스레드 환경으로 인해 최적화의 중요성이 더욱 커진다. 따라서 단순히 런타임 성능뿐만 아니라 에셋 구성, 로딩 방식, 데이터 처리까지 전반적인 최적화가 필요하다.

 

 

1. 게임 에셋 최적화

  가장 기본이 되는 최적화는 에셋 자체를 줄이는 것이다. 텍스처와 모델은 게임에서 가장 많은 메모리를 차지하는 요소이기 때문에, 압축 포맷을 사용하거나 해상도를 줄이고, 모델의 폴리곤 수를 낮추는 방식으로 데이터를 줄여야 한다. 이는 메모리 절약뿐만 아니라 다운로드 크기 감소와 로딩 속도 개선에도 직접적인 영향을 준다.

 

 

2. 오브젝트 풀링(Object Pooling)

  Unity에서 객체를 생성하고 파괴하는 과정은 힙 메모리를 사용하며 GC를 유발한다. WebGL 환경에서는 GC가 메인 스레드를 멈추기 때문에 프레임 드랍이 크게 체감된다. 따라서 오브젝트 풀링을 활용해 객체를 재사용하는 방식이 중요하다. 이는 런타임에서 발생하는 불필요한 메모리 할당을 줄이고, GC 발생 빈도를 낮추는 데 효과적이다.

 

  다만 오브젝트 풀링을 적용할 때 풀의 크기를 무작정 크게 잡으면 오히려 초기 메모리 사용량이 증가할 수 있다. 특히 WebGL 환경에서는 힙 메모리가 제한되어 있기 때문에, 필요한 만큼만 생성하도록 설계하는 것이 중요하다. 또한 생성 비용이 크지 않거나, 사용 빈도가 낮은 오브젝트까지 풀링하면 오히려 관리 비용만 증가할 수 있기 때문에 총알, 이펙트처럼 반복 생성이 빈번한 객체에 우선적으로 적용하는 것이 효율적이다.

 

 

3. URP 및 SRP Batcher 활용

  Unity의 SRP Batcher는 동일한 머티리얼을 사용하는 오브젝트들을 묶어서 처리함으로써 렌더링 시 상태 변경 횟수를 줄여준다. 이를 통해 CPU에서 발생하는 렌더링 비용을 효과적으로 감소시킬 수 있다. 특히 오브젝트 수가 많은 씬에서 성능 향상 효과가 크게 나타난다.

 

 

4. 오클루전 컬링(Occlusion Culling)

  오클루전 컬링은 카메라에 보이지 않는 오브젝트를 렌더링하지 않도록 하는 기술이다. 예를 들어 벽 뒤나 건물 뒤에 가려진 오브젝트는 실제로 화면에 보이지 않지만, 별도의 처리를 하지 않으면 계속 렌더링된다. 오클루전 컬링을 적용하면 이러한 불필요한 GPU 연산을 줄일 수 있어 성능 개선에 도움이 된다.

 

 

5. LOD(Level of Detail) 시스템

  LOD 시스템은 카메라와의 거리에 따라 오브젝트의 디테일을 조절하는 방식이다. 가까이 있는 오브젝트는 높은 디테일을 유지하고, 멀리 있는 오브젝트는 저해상도 모델로 교체함으로써 렌더링 부담을 줄인다. 이를 통해 시각적인 품질을 크게 해치지 않으면서도 GPU 사용량을 효과적으로 줄일 수 있다.

 

 

6. 조명 베이크(Baked Lighting)

  실시간 조명은 연산 비용이 높기 때문에 WebGL 환경에서는 부담이 크다. 따라서 가능한 경우 라이트맵과 라이트 프로브를 활용해 조명 정보를 사전에 계산해 두는 것이 좋다. 이렇게 하면 런타임에서의 조명 계산이 줄어들어 전체적인 성능을 안정적으로 유지할 수 있다.

 

 

7. 문자열 생성 최소화

  C#에서 문자열은 참조 타입이기 때문에 새로운 문자열을 생성할 때마다 힙 메모리를 사용하게 되고, 이는 GC를 유발한다. 따라서 반복적으로 문자열을 생성하는 로직은 피하는 것이 중요하다. 런타임에 문자열을 빌드해야할 경우 StringBuilder를 사용하거나, JSON이나 XML과 같은 텍스트 기반 데이터 대신 Scriptable Object / MessagePack / Protobuf 같은 포맷 바이너리 포맷을 사용하는 것도 성능 개선에 도움이 된다.

 

 

8. Addressable Asset System 활용

  WebGL에서는 모든 리소스를 한 번에 로드하는 방식이 비효율적이다. Addressable을 활용하면 필요한 시점에 필요한 에셋만 비동기적으로 로드할 수 있다. 또한 CDN을 통한 원격 로딩도 가능하기 때문에 초기 로딩 시간을 줄이고 메모리 사용량을 효율적으로 관리할 수 있다.

 

 

9. 포스트 프로세싱 효과 제한

  블룸이나 모션 블러와 같은 포스트 프로세싱 효과는 전체 화면에 적용되기 때문에 GPU 비용이 크다. WebGL 환경에서는 이러한 효과가 성능 저하의 주요 원인이 될 수 있으므로, 꼭 필요한 경우에만 제한적으로 사용하는 것이 좋다.

 

 

 

 

결론적으로..

  Unity WebGL 최적화는 특정 기술 하나만 적용한다고 해결되는 문제가 아니다. 에셋, 메모리, 렌더링, 데이터 처리 등 다양한 요소를 종합적으로 고려해야 하며, 각 요소가 서로 영향을 주기 때문에 전체적인 균형이 중요하다. 특히 WebGL 환경에서는 “필요한 것만, 최소한의 비용으로 처리한다”는 원칙이 핵심이며, 이를 기반으로 설계를 진행하는 것이 안정적인 성능을 확보하는 데 중요하다.

  사실 위 내용은 꼭 WebGL 환경이 아니라도 사용하는 최적화 방법들이기에,, 다양한 것들을 고려해서 개발하면 좋을 것 같다!

 

 

 

Reference

https://unity.com/how-to/profile-optimize-web-build#the-built-in-render-pipeline-or-urp?

 

웹 빌드를 프로파일링하고 최적화 (하다) 방법 | Unity

Unity 웹 프로젝트 최적화(하다) 팁을 확인하세요. 웹 게임이 웹사이트와 소셜 미디어에 효율적으로 배포되고 임베드되도록 최적화해야 합니다.

unity.com

 

 

 

 

Direct3D 12 graphics Pipeline

지난 포스팅에서는 pixel shader가 하는 일 중 texturing과 관련해서 texel과 pixel의 해상도 차이로 인한 확대,축소 문제를 어떻게 해결하는지 살펴봤다. 오늘 다룰 내용은 Lighting이다. 전통적인 phong 모델부터 per-pixel lighting, blinn-phong model까지 살펴볼 예정이다. 

 

 

 

 

 

Phong Lighting Model

◇  Diffuse / Specular / Ambiend / Emissive 4가지를 섞어 그럴듯하게 빛을 표현하자

  • 가장 유명하고 전통적인 방식이다.
  • approximation을 어떻게 잘 하는지가 중요하다. 

 

 

 

Light Sources

◇ Point light

  • 하나의 point로부터 전방향으로 빛을 뿜어낸다. 
  • point로부터 거리에 따라 intensity가 결정된다

Directional light

  • scene으로부터 멀리 떨어진 곳에서 보내는 빛  ex) 태양
  • scene 전체에서 one direction이다.
  • 내가 어디에 존재하든 일정한 direction과 크기를 가진다 

◇ Spotlight

  • cone shaped light
  • 거리와 각도에 따라 intensity가 달라진다.
  • ex) 형광등, 백열등

◇ Ambient

  • 은은하게 밝혀주는 간접광
  • 반사되어 떠도는 빛을 계산하기 어렵기 때문에 constant value로 뭉개서 계산한다.

 

 

 

Diffuse (난반사광)

물체 표면에 빛이 반사되었을 때 전방향으로 uniform하게 퍼지는 것을 modeling한다.

  • Lambert's law를 따른다  = 입사각이 작을 수록 밝게 보인다.
    • Diffuse 조명은 표면의 밝기가 빛의 입사각 cos(θ)에 비례한다. 
    • θ: 빛의 방향과 표면 법선 벡터(normal) 사이의 각도
    • 관찰자의 위치와 밝기는 무관하다. 

  • Diffuse computation

  • n : surface normal (표면 법선 벡터)
  • l : light direction (빛 방향 벡터)
  • n · l : 두 벡터의 내적
  • max(n·l, 0) : 음수 제거
  • s_d : light color (빛 색)
  • m_d : material diffuse color (물체 색)

 

white light (1,1,1)이 들어올 때, 물체가 노란색이면 R,G값은 반사하고 B는 흡수한다.

 

 

 

Specular (정반사광)

 

 물체 표면에 빛이 닿을 때, 특정 방향으로 강하게 반사되는 조명 효과

  • 표면 특정 부분에 highlight 효과를 줄 수 있다. 
  • 관찰자의 방향에 따라 밝기가 달라진다.
    •  : view vector
    •  : reflection vector
    •  : shininess (클수록 날카롭고 작은 하이라이트)
    •  : light source의 gray-scale value
    •  : highlight on the surface

 

 

Ambient (간접광)

주변에 퍼져있는 일관된 빛 

  • 씬 내의 다양한 물체에 반사된 빛
  • 표면에 도달한 주변 조명은 모든 방향에서 동일한 강도로 산란된다.

 

 

Emissive 

표면 자체에서 방출되는 빛

emissive 적용 전(좌), emissive 적용 후(우)

 

 

 

Phong Lighting Model

 

◇  Diffuse / Specular / Ambiend / Emissive 4가지 term을 합쳐서 표현한다.

 

 

 

Per-pixel Lighting

◇  각 픽셀 단위에서 조명을 계산하는 방식

  • per-vertex 방식에 비해 high quality
  • per-vertex 방식은 정점만 밝기 계산을 하고, 그 사이 픽셀은 선형 보간해서 부드럽지 않은 부분이 생긴다.

per-vertex(좌), per-pixel(우)

 

◇  pixel shader에서 per-pixel 조명 계산을 위해 필요한 벡터

  • l : light vector
  • n : normal
  • v : view vector
  • r : reflection vector  ( r = 2n(n·l) - l )
  • (이때 l은 directional light로 모든 surface에서 동일 / n,r,v는 픽셀마다 다름)
  • 중요한 점은 l이 world space 기준이기 때문에 n,v도 world space 기준 좌표가 필요하다!
  • n과 v는 vertex shader에서 pixel shader로 넘겨주는 값이고, r은 직접 계산이 가능하기 때문에 파라메터를 하나 줄일 수 있다. 

◇  Normal이 pixel마다 생기는 과정

  • vertex shader : vertex normal을 world space로 변환
  • rasterizer : vertex normal을 보간  

◇  View vector가 pixel마다 생기는 과정

  • vertex shader : v = cameraPos - vertexPos (world space 좌표임)
  • rasterizer : vector 보간

◇   최종 pixel 별 Phong lighting 계산

  • n,v,l이 준비되었기 때문에 r은 계산을 통해서 구할 수 있다.
  • 다음 최종적으로 pixel마다 phong lighting을 계산하면 per-pixel을 구현할 수 있게 된다.

 

 

 

 

 

Blinn-Phong Reflection Model

◇  Phong model의 modified(수정된) version

  • Phong은 lighting approximation에 효율적이다.
  • 그러나 specular term에서 가끔 unrealistic한 결과를 낸다.

 

  • dot project는 cos 계산이기 때문에 90도를 넘기면 음수를 반환하고, 그대로 max()를 통과하면 0이 된다.
  • 이런 급격한 cut off 때문에 경계(boundray)가 생긴다.

 

 

◇  Blinn-Phong reflection model - specular term

  • reflection vector(r) 대신에 halfway vector(h)를 이용한다.
  • h :  (view vector) 와  (light vector)을 이등분(bisects)하는 벡터
  • blinn phong의 장점
    • r과 v의 차이가 90도가 넘더라도, h와 n 사이 각도는 90도가 넘지 않기 때문에 이전의 문제점이 해결된다! 
    • 추가로 r을 구하기 위해서 내부적으로 내적을 했었는데, 이 과정이 없어지고 단순한 덧셈 연산으로 중간 벡터를 구하기 때문에 처리도 빨라진다. 

 

  • 적용 결과
    • specular term을 개선한 것이기 때문에, 바다에 반사되는 햇빛이나 젖은 땅에 멀리 떨어진 가로등이 반사되는 등 평면에 대한 반사를 표현할 때 훨씬 현실적인 표현이 가능해진다고 한다. 

 

 

 

 

 

 

Reference KHU 강형엽 교수님 강의를 수강하며 정리한 내용입니다.

'그래픽스' 카테고리의 다른 글

[DirectX12] 5. Output Merger  (1) 2026.04.10
[DirectX12] 3. Pixel Shader - Texturing  (0) 2026.03.13
[DirectX12] 2. Rasterizer  (1) 2026.03.13
[DirectX12] 1. Input Assembler, Vertex Shader  (0) 2026.03.12

 

Direct3D 12 graphics Pipeline

Rasterizer를 통해 pixel이 생성되고, 일부 값들을 보간해서 채워줬다. 이제 Pixel shader에서는 그 위에 Texturing과 Lighting 작업을 해준다. 이번 포스팅에서는 그 중에서 Texturing에 대해 알아볼 예정이다.

 

 

 

 

Image Texturing

◇  rasterizer가 보간한 texture 좌표에 texture 해상도를 곱해서 실제 texture에서 rgb값을 꺼내오는 것

  • 1) 모델링 단계에서 polygon mesh의 각 정점마다 texture coordination가 설정되어야 한다. (s,t)
  • 2) 이후 rasterizer가 각 pixel에 따라 interpolation 해서 넘겨준다. 
  • 3) texture 좌표 (s,t)에 실제 texture의 해상도를 곱해서 (s', t')를 구한다.
  • 4) (s', t') = c의 좌표를 활용해서 실제 texture(c)에서 rgb 값을 가져온다. 

(a) 정점 별 texture 좌표 (b) 보간된 texture 좌표 (c) texture 해상도를 곱한 후

 

  • texture coordintate의 s와 t는 0과 1 사이로 nomalize 되어있어 다양한 텍스쳐에 적용될 수 있다.

 

 

 

 

Surface Parameterization

◇  복잡한 2D,3D 표면을 단순한 2D 좌표계 (s,t)로 표현하는 방법 

 

 

 

 

Chart and Atlas

복잡한 polygon mesh는 patch(ex.머리, 몸 , 팔)들로 나누어 관리한다.

  • 각 patch들로 나누어 surface parameterization을 수행한다.
  • chart : 각 patch들의 이미지
  • atlas : 여러 개의 chart들이 texture에 합쳐진 것

 

 

 

 

Texture Wrapping

range를 벗어난 (s,t)들을 처리해주는 방법

  • Clamp-to-Edge (c) : 벗어날 경우 edge color로 렌더링한다.
  • Repeat (d) : 반복으로 타일링하는 방식
  • Mirrored-Repeat (e),(f) : 그냥 Repeat을 하면 경계가 부자연스러울 수 있기 때문에 mirrored 방식을 사용할 수 있다.

 

 

 

 

Texture Filtering

텍셀(texel) → 화면 픽셀(pixel)로 맵핑할 때, 해상도 차이를 보정하는 기술 

  • 텍스처 해상도와 화면 해상도가 다를 경우 생기는 왜곡/노이즈 문제가 발생할 수 있다. 
  • Magnification (확대) : pixel > texel

 

  • Minification (축소) : pixel < texel

 

 

Filtering for Magnification

pixel이 texel보다 많은 경우 = 하나의 texel이 여러 pixel을 표현하게 된다.

  • Option 1: Nearest point sampling
    • 현재 픽셀에 가장 가까운 텍셀 하나의 값을 그대로 가져온다. 
    • 가장 간단하지만 같은 texture 값을 여러 번 가져오면서 block image 문제가 발생한다.

  • Option 2 : Bilinear interpolation
    • 주변 4개의 텍셀 값을 이용해 보간(Interpolation)하여 중간색을 계산
    • 수평/수직 방향으로 보간을 2번 해서 최종 c를 구한다.
    • 경계가 부드럽고 자연스러운 색 표현이 가능하지만, 연산량이 많은 단점이 있다.

 

 

 

Filtering for Minification

 pixel이 texel보다 적은 경우, texel의 숫자를 줄여야 한다! = 확대보다 심각한 문제 

  • Aliasing 문제 
    • 어떤 texel을 샘플링하느냐에 따라 결과가 달라지는 aliasing 문제가 발생한다.
    • 표현하고자 하는 texture와 전혀 다르게 표현될 수 있는 심각한 문제다! 
    • 이를 위해 Mipmapping을 활용할 예정 

 

 

 

Mipmapping

Aliasing 문제를 해결하기 위한 방법 - down sampling을 통해 minmap을 만들자!

  • 목표 : texel의 수를 줄여서 pixel 수에 최대한 가까워지게 할 것
  • mipmap : downsampling 한 level-λ texture의 모음
  • 원래 텍스처의 해상도가 인 경우, (l+1)개의 레벨로 구성된 피라미드(mipmap)를 구축한다.

  • 이때 각 pixel에 맞는 level-λ texture는 각 pixel이 차지하는 texel의 가로 or 세로 개수 m을 log_2(m)으로 표현하면 해당 level이 된다. 즉, λ = log_2(m) 이다. 
  • 로그 계산이 딱 떨어지면 좋겠지만, 아래 처럼 m=3인 예시에서만 봐도 문제가 발생한다.
  • Option 1) 가장 가까운 level 고르기.

m = 3인 경우

 

 

 

 

 

 

Reference KHU 강형엽 교수님 강의를 수강하며 정리한 내용입니다.

'그래픽스' 카테고리의 다른 글

[DirectX12] 5. Output Merger  (1) 2026.04.10
[DirectX12] 4. Pixel Shader - Lighting  (0) 2026.03.13
[DirectX12] 2. Rasterizer  (1) 2026.03.13
[DirectX12] 1. Input Assembler, Vertex Shader  (0) 2026.03.12

+ Recent posts