본문 바로가기
공부/Graphics

[Graphics] 포워드 렌더링(Forward Rendering) & 디퍼드 렌더링(Deferred Rendering)

by MY블로그 2023. 6. 27.

출처 : https://lettier.github.io/

포워드 렌더링(Forward Rendering)

포워드 렌더링은 컴퓨터 그래픽스에서 사용되는 렌더링 기술 중 하나입니다.

전통적인 렌더링 기법이며 각각의 빛이나 그림자를 계산하는 대신 모든 빛과 재질에 대한 계산을 한번에 수행 합니다.

출처 : https://mkblog.co.kr/gpu-forward-rendering-vs-deferred-rendering/

 

포워드 렌더링은 다음과 같은 단계로 진행됩니다.

1. Scene 그래픽 데이터 준비

렌더링할 객체의 정점 버퍼와 인덱스 버퍼를 생성하고, 재질 및 텍스처 정보를 설정 합니다.

2. 카메라 설정

뷰포트 설정, 카메라 위치와 방향 설정 등을 진행 합니다.

3. 그림자 계산

포워드 렌더링에서는 그림자를 계산하기 위해 텍스처 매핑이 사용될 수 있습니다.

그림자 맵을 만들고, 적절한 투영 및 텍스처 매핑 기법을 사용하여 그림자를 계산하게 됩니다.

4. 조명 계산

포워드 렌더링에서는 모든 조명 계산을 한번에 묶음 수행 합니다.

조명의 위치, 색상, 강도 등을 설정하고 조명 모델을 사용하여 적절한 조명 계산을 수행합니다.

5. 정점 변환

카메라 공간에서 월드 공간으로의 정점 변환을 수행합니다.

이 단게에서는 정점의 위치와 방향을 변환하여 월드 공간에서의 정점 위치를 계산하게 됩니다.

6. 정점 쉐이더 실행

정점 쉐이더는 각각의 정점마다 실행되는 작은 프로그램입니다.

정점의 위치, 색상, 텍스처좌표 등을 계산합니다.

이 단계에서는 정점마다의 변환을 수행하며 텍스처 좌표를 계산하고 조명 모델에 따라 조명 계산이 수행되는 단계 입니다.

7. 픽셀 쉐이더 실행

픽셀 쉐이더는 각 픽셀에 대해 실행되는 작은 프로그램입니다.

최종적인 픽셀의 색상을 계산하는 단계이며, 이전단계인 정점 쉐이더 단계에서 계산한 결과를 보간하여 픽셀 쉐이더에 전달하고 조명 계산 결과를 이용하여 최종 픽실 색상을 계산합니다.

8. 렌더 타겟에 출력

위의 모든 단계가 끝나면 계산된 픽셀의 색상을 렌더 타겟에 출력하여 화면에 나타나는 단계입니다.

 

포워드 렌더링은 간단하고 직관적인 방법이지만, 조명과 재질이 복잡하고 많은 경우에는 계산량이 많아질 수 있습니다. 때문에 대규모의 조명이나 그림자가 필요한 시나리오등에서는 다른 렌더링 기법인 디퍼드 렌더링(Deferred Rendering) 혹은 레이 트레이싱(Ray Tracing)과 같은 기술이 더 효율적일 수 있습니다.

 

포워드 렌더링의 장단점

장점

1. 간단하고 직관적인 구현

오래전부터 사용된 전통적인 렌더링 기법이므로 구현이 비교적 간단합니다.

초기 개발자에게 접근하기 쉽고, 빠르게 결과물을 얻을 수 있습니다.

 

2. 작은 메모리 요구량

렌더링하는 동안에 필요한 메모리 요구량이 상대적으로 작습니다.

렌더타겟에 직접 결과를 출력하게되므로 중간 결과를 저장하는 추가적인 버퍼를 사용하지 않습니다.

 

3. DirectX11 하드웨어 호환성

DX11과 함께 사용하기 좋아 호환성이 뛰어납니다.

그래픽스 API로 많은 하드웨어에서 효율적으로 작동합니다.

단점

1. 조명 및 그림자 처리의 제한

모든 빛과 재질에 대한 계산을 한번에 묶음 수행하기 때문에 조명이나 그림자 계산에 제한적 입니다.

큰 규모의 조명이나 복잡한 그림자를 처리하기에는 적합하지 않습니다.

 

2. 오버드로우(Overdraw)

모든 물체에 대하여 빛 계산을 수행하기 때문에 화면에 실제로 보이지 않는 물체에 대해서도 렌더링 작업을 수행하게 됩니다.

때문에 오버드로우가 발생하며 그에 따라 GPU의 부하가 증가할 수 있습니다.

 

3. 투명한 객체 처리의 어려움

투명한 객체 처리에 제한이 있습니다.

투명한 물체는 알파 블렌딩과 정렬 등의 추가적인 처리가 필요하게 되며 복잡한 투명 효과를 구현하기 어렵습니다.

 

4. 성능 저하

대규모의 조명이나 그림자가 필요한 시나리오에서는 성능 저하가 발생하게 됩니다.

많은 조명이나  그림자 계산은 각각의 모든 연산을 통해야 하기 때문에 CPU & GPU의 부하가 발생합니다.


디퍼드 렌더링(Deferred Rendering)

디퍼드 렌더링도 포워드 렌더링과 마찬가지로 컴퓨터 그래픽스에서 사용되는 렌더링 기법중 하나 입니다.

포워드 렌더링과는 달리 조명 계산을 후반으로 미루어 진행하는 기법입니다.

출처 : https://mkblog.co.kr/gpu-forward-rendering-vs-deferred-rendering/

디퍼드 렌더링은 다음과 같은 단계로 진행됩니다.

 

1. 지오메트리 패스(Geometry Pass)

Scene의 지오메트리를 렌더링하여 버퍼에 저장하는 단계입니다.

버퍼에는 정점의 위치, 법선, 색상(알베도)등의 정보가 저장 됩니다.

2. G Buffer 생성

디퍼드 렌더링에서는 G버퍼라고 불리는 여러 개의 버퍼를 생성합니다.

G버퍼에는 정점의 위치, 법선, 색상(알베도)등의 정보를 저장합니다.

일반적으로는 정점 위치와 법선을 저장하는 G버퍼와 색상(알베도)와 추가적인 정보를 저장하는 G버퍼가 사용됩니다.

3. 조명 계산

디퍼드 렌더링에서는 조명 계산을 직접적으로 수행하지 않습니다.

그대신 G버퍼에 저장된 정보를 이용하여 조명 계산을 수행 합니다.

조명 정보를 이용하여 조명 모델에 따라 조명 계산을 수행하고 그 결과를 임시 버퍼에 저장합니다.

4. 라이트 패스(Light Pass)

G버퍼를 이용한 조명 계산 단계가 완료 된 후 라이트 패스를 수행합니다.

라이트 패스 단계에서는 임시 버퍼에 저장된 조명 계산 결과와 G버퍼의 정보를 이용하여 최종적인 픽셀 색상을 계산하게 됩니다.

디퍼드 렌더링에서는 픽셀 쉐이더를 사용하여 각 픽셀의 색상을 계산합니다.

5. 렌더 타겟에 출력

최종적으로 계산된 픽셀의 색상을 렌더 타겟에 출력하여 화면에 나타나는 단계입니다.

 

디퍼드 렌더링은 복잡한 조명과 투명한 객체 처리가 필요한 시나리오에서 유용한 기법 입니다.

하지만 추가적인 메모리 요구량과 구현의 복잡성을 고려해야 합니다.

 

디퍼드 렌더링의 장단점

장점

1. 조명 계산의 효율성

디퍼드 렌더링은 조명 계산을 후반으로 미루기 때문에 조명이 많고 복잡한 시나리오에서도 상대적으로 더 효율적인 처리가 가능하게 됩니다.

조명 계산을 더 효율적으로 분산이 되고 복잡한 조명 모델을 구현하는데 유용 합니다.

 

2. 투명한 객체 처리

투명한 객체를 처리하는 데 용이합니다.

G버퍼에 투명한 객체의 정보를 저장하고, 라이트 패스 단계에서 적절한 블렌딩 기법을 사용한다면 투명한 효과를 구현할 수 있습니다.

 

3. 오버드로우(Overdraw) 감소

디퍼드 렌더링은 G버퍼를 사용하여 픽셀의 정보를 한번에 저장합니다.

때문에 포워드 렌더링에서 발생하는 오버드로우 문제를 완화 시킬 수 있습니다.

단점

1. 추가적인 메모리 요구

디퍼드 렌더링은 G버퍼를 생성하여 정보를 저장하기 때문에 메모리 요구량이 증가합니다.

G버퍼의 크기가 크면 클 수록 추가적인 메모리의 크기가 커지게 됩니다.

 

2. 복잡한 구현 방식

디퍼드 렌더링은 포워드 렌더링보다 구현방식이 복잡 합니다.

여러개의 버퍼를 생성해야하고 조명 계산과 라이트 패스에서 추가적인 처리를 수행해야 합니다.

이로 인해 초기 개발이 어려울 수 있습니다.

 

3. 하드웨어 요구 사항

디퍼드 렌더링은 추가적인 버퍼와 계산이 필요하므로 충분한 메모리 공간 확장을 위한 하드웨어가 필요하게 됩니다. 때문에 일부 하드웨어(저성능)에서는 성능 저하가 발생할 수 있습니다.

따라서 하드웨어 요구 사항이 증가 할 수 있습니다.

 


포워드 렌더링 & 디퍼드 렌더링 두개의 간단한 코드 예제

#include <Windows.h>
#include <d3d11.h>
#include <DirectXMath.h>

#pragma comment(lib, "d3d11.lib")

// 전역 변수
HWND g_hWnd;
ID3D11Device* g_pd3dDevice = nullptr;
ID3D11DeviceContext* g_pd3dDeviceContext = nullptr;
IDXGISwapChain* g_pSwapChain = nullptr;
ID3D11RenderTargetView* g_pRenderTargetView = nullptr;

// 초기화 함수
bool InitD3D(HWND hWnd)
{
    DXGI_SWAP_CHAIN_DESC sd = {};
    sd.BufferCount = 1;
    sd.BufferDesc.Width = 800;
    sd.BufferDesc.Height = 600;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;

    D3D_FEATURE_LEVEL featureLevel;
    HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    if (FAILED(hr))
        return false;

    ID3D11Texture2D* pBackBuffer;
    g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
    g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_pRenderTargetView);
    pBackBuffer->Release();

    g_pd3dDeviceContext->OMSetRenderTargets(1, &g_pRenderTargetView, nullptr);

    D3D11_VIEWPORT viewport;
    viewport.Width = static_cast<float>(sd.BufferDesc.Width);
    viewport.Height = static_cast<float>(sd.BufferDesc.Height);
    viewport.MinDepth = 0.0f;
    viewport.MaxDepth = 1.0f;
    viewport.TopLeftX = 0;
    viewport.TopLeftY = 0;

    g_pd3dDeviceContext->RSSetViewports(1, &viewport);

    return true;
}

// 렌더링 함수
void Render()
{
    // 렌더링 코드 작성

    // 화면에 그리기
    g_pSwapChain->Present(0, 0);
}

// 메시지 처리 함수
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

// 진입점
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // 윈도우 클래스 등록
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.lpszClassName = "D3D11App";
    RegisterClassEx(&wcex);

    // 윈도우 생성
    g_hWnd = CreateWindowEx(0, wcex.lpszClassName, "DirectX 11 App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);

    if (!InitD3D(g_hWnd))
        return -1;

    ShowWindow(g_hWnd, nCmdShow);

    // 메시지 루프
    MSG msg = {};
    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            Render();
        }
    }

    // 정리 작업
    g_pRenderTargetView->Release();
    g_pSwapChain->Release();
    g_pd3dDeviceContext->Release();
    g_pd3dDevice->Release();

    return static_cast<int>(msg.wParam);
}

// 디퍼드 렌더링
#include <Windows.h>
#include <d3d11.h>
#include <DirectXMath.h>

#pragma comment(lib, "d3d11.lib")

// 전역 변수
HWND g_hWnd;
ID3D11Device* g_pd3dDevice = nullptr;
ID3D11DeviceContext* g_pd3dDeviceContext = nullptr;
IDXGISwapChain* g_pSwapChain = nullptr;
ID3D11RenderTargetView* g_pRenderTargetView = nullptr;
ID3D11Texture2D* g_pDepthStencilBuffer = nullptr;
ID3D11DepthStencilView* g_pDepthStencilView = nullptr;
ID3D11ShaderResourceView* g_pGBufferView[2] = { nullptr, nullptr }; // G 버퍼 뷰

// 초기화 함수
bool InitD3D(HWND hWnd)
{
    DXGI_SWAP_CHAIN_DESC sd = {};
    sd.BufferCount = 1;
    sd.BufferDesc.Width = 800;
    sd.BufferDesc.Height = 600;
    sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    sd.OutputWindow = hWnd;
    sd.SampleDesc.Count = 1;
    sd.SampleDesc.Quality = 0;
    sd.Windowed = TRUE;

    D3D_FEATURE_LEVEL featureLevel;
    HRESULT hr = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);
    if (FAILED(hr))
        return false;

    ID3D11Texture2D* pBackBuffer;
    g_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer);
    g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_pRenderTargetView);
    pBackBuffer->Release();

    // 깊이 스텐실 버퍼 생성 및 뷰 연결
    D3D11_TEXTURE2D_DESC depthStencilDesc = {};
    depthStencilDesc.Width = sd.BufferDesc.Width;
    depthStencilDesc.Height = sd.BufferDesc.Height;
    depthStencilDesc.MipLevels = 1;
    depthStencilDesc.ArraySize = 1;
    depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
    depthStencilDesc.SampleDesc = sd.SampleDesc;
    depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
    depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    g_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, &g_pDepthStencilBuffer);
    g_pd3dDevice->CreateDepthStencilView(g_pDepthStencilBuffer, nullptr, &g_pDepthStencilView);

    g_pd3dDeviceContext->OMSetRenderTargets(1, &g_pRenderTargetView, g_pDepthStencilView);

    D3D11_VIEWPORT viewport;
    viewport.Width = static_cast<float>(sd.BufferDesc.Width);
    viewport.Height = static_cast<float>(sd.BufferDesc.Height);
    viewport.MinDepth = 0.0f;
    viewport.MaxDepth = 1.0f;
    viewport.TopLeftX = 0;
    viewport.TopLeftY = 0;

    g_pd3dDeviceContext->RSSetViewports(1, &viewport);

    // G 버퍼 생성 및 뷰 연결
    D3D11_TEXTURE2D_DESC gBufferDesc = {};
    gBufferDesc.Width = sd.BufferDesc.Width;
    gBufferDesc.Height = sd.BufferDesc.Height;
    gBufferDesc.MipLevels = 1;
    gBufferDesc.ArraySize = 1;
    gBufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    gBufferDesc.SampleDesc = sd.SampleDesc;
    gBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    gBufferDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
    g_pd3dDevice->CreateTexture2D(&gBufferDesc, nullptr, &g_pGBuffer[0]);
    g_pd3dDevice->CreateTexture2D(&gBufferDesc, nullptr, &g_pGBuffer[1]);

    // G 버퍼 뷰 생성
    D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
    srvDesc.Format = gBufferDesc.Format;
    srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
    srvDesc.Texture2D.MipLevels = 1;
    srvDesc.Texture2D.MostDetailedMip = 0;
    g_pd3dDevice->CreateShaderResourceView(g_pGBuffer[0], &srvDesc, &g_pGBufferView[0]);
    g_pd3dDevice->CreateShaderResourceView(g_pGBuffer[1], &srvDesc, &g_pGBufferView[1]);

    return true;
}

// G 버퍼 렌더링 함수
void RenderGBuffer()
{
    // G 버퍼 렌더링 코드 작성
}

// 라이트 패스 렌더링 함수
void RenderLightPass()
{
    // 라이트 패스 렌더링 코드 작성
}

// 전체 렌더링 함수
void Render()
{
    // G 버퍼 렌더링
    RenderGBuffer();

    // 라이트 패스 렌더링
    RenderLightPass();

    // 화면에 그리기
    g_pSwapChain->Present(0, 0);
}

// 메시지 처리 함수
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

// 진입점
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // 윈도우 클래스 등록
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.lpszClassName = "D3D11App";
    RegisterClassEx(&wcex);

    // 윈도우 생성
    g_hWnd = CreateWindowEx(0, wcex.lpszClassName, "DirectX 11 App", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);

    if (!InitD3D(g_hWnd))
        return -1;

    ShowWindow(g_hWnd, nCmdShow);

    // 메시지 루프
    MSG msg = {};
    while (msg.message != WM_QUIT)
    {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            Render();
        }
    }

    // 정리 작업
    g_pRenderTargetView->Release();
    g_pDepthStencilBuffer->Release();
    g_pDepthStencilView->Release();
    g_pGBuffer[0]->Release();
    g_pGBuffer[1]->Release();
    g_pGBufferView[0]->Release();
    g_pGBufferView[1]->Release();
    g_pSwapChain->Release();
    g_pd3dDeviceContext->Release();
    g_pd3dDevice->Release();

    return static_cast<int>(msg.wParam);
}

위의 두 기본 예제 코드의 차이점

1. 초기화 함수 - InitD3D

포워드 렌더링 : 단순히 백 버퍼와 렌더 타겟 뷰를 생성 및 연결합니다.

디퍼드 렌더링 : 추가로 깊이 스텐실 버퍼, 깊이 스텐실 뷰, G버퍼용 텍스처 뷰, 뷰 생성 및 연결합니다.

2. 렌더링 함수 - Render

포워드 렌더링 : 단일 렌더링 패스에서 화면을 그립니다.

디퍼드 렌더링 : G버퍼 렌더링 패스와 라이트 패스로 나뉩니다. RenderGBuffer 함수에서 G버퍼에 정보를 채우고, RenderLightPass 함수에서는 조명 계산 등을 수행합니다.

3. 추가적인 변수 및 자원

포워드 렌더링 : 추가적인 변수, 자원이 없습니다.

디퍼드 렌더링 : G버퍼용 텍스처와 뷰, G버퍼 뷰를 추가적으로 선언하고 있습니다.

4. 주된 차이점

포워드 렌더링 : 각각의 객체에 따로 렌더링 패스를 반복 및 조명 계산을 수행하고 있습니다.

디퍼드 렌더링 : G버퍼에 객체의 정보를 저장, 그 후에 조명 계산을 수행하고 있습니다.

 


 

[참고]

 

[GPU] Forward Rendering vs. Deferred Rendering – MKBlog

예전에 FlexRendering에서 Deferred Rendering에 대해 작성한 적이 있다. FlexRendering에서 사용하는 Deferred Rendering은 Tile-based Rendering을 의미하는 것 같다. 하지만, 보통 Graphic 연산에서 Deferred Rendering은 다른

mkblog.co.kr

 

Deferred Rendering | 3D Game Shaders For Beginners

Interested in adding textures, lighting, shadows, normal maps, glowing objects, ambient occlusion, reflections, refractions, and more to your 3D game? Great! 3D Game Shaders For Beginners is a collection of shading techniques that will take your game visua

lettier.github.io

 

댓글