Back to notes
    개발 일지엔진렌더링그래픽스작성 · 2026-06-30

    우리 엔진 탐방기: 한 프레임 동안 일어나는 일들

    ALEngine의 렌더링 파이프라인

    목차

    게임은 한 프레임 동안 무슨 일이 일어날까?

    왜 이 글을 쓰게 되었을까?

    게임을 하다 보면 버튼을 누르는 순간 캐릭터가 움직이고, 화면은 자연스럽게 바뀐다. 너무 당연하게 보이지만, 실제로는 한 장의 화면을 만들기 위해 CPU와 GPU가 생각보다 많은 일을 한다. 처음 Vulkan을 공부할 때 가장 먼저 놀랐던 건 삼각형 하나를 그리는 코드가 너무 길다는 점이었다.

    그때는 “삼각형 하나 그리는데 왜 이렇게까지 해야 하지?”라는 생각을 많이 했다. 그런데 직접 엔진을 만들다 보니, 화면 하나가 만들어지는 과정은 단순히 모델 하나를 그리는 일이 아니었다.

    배경을 준비하고, 그림자를 만들고, 모델의 재질 정보를 저장하고, 조명을 계산하고, 에디터 UI까지 얹은 뒤에야 비로소 화면에 보이는 한 프레임이 완성된다. AL Engine의 렌더링 목표 중 하나는 약 1000개의 오브젝트를 동시에 렌더링하면서도 60FPS를 방어하는 것이었다. 그래서 이 글에서는 우리가 만든 AL Engine에서 한 프레임이 어떤 순서로 만들어지는지, 그리고 이 목표가 렌더링 구조에 어떤 영향을 줬는지 정리해보려고 한다.


    먼저 전체 흐름부터 보기

    렌더링 파이프라인
    렌더링 파이프라인

    AL Engine의 한 프레임은 크게 다음 순서로 만들어진다.

    cpp
    drawFrame()
    {
        renderBackground();
        renderShadowMap();
    
        renderGeometryPass();
        renderLightingPass();
    
        renderCollider();
        renderGUI();
    
        present();
    }
    

    처음 보면 단계가 많아 보이지만, 크게 나누면 어렵지 않다. 먼저 배경과 그림자처럼 렌더링에 필요한 정보를 준비한다. 그다음 모델을 그리면서 필요한 재질 정보를 G-Buffer에 저장한다.

    마지막으로 저장된 정보를 바탕으로 조명을 계산하고, 에디터 UI를 얹어 화면에 출력한다. 이 순서에는 나름의 이유가 있다. 그림자를 계산하려면 먼저 빛의 시점에서 깊이 정보를 만들어야 하고, 조명을 계산하려면 먼저 화면에 보이는 픽셀의 위치, 노멀, 재질 정보가 필요하다. 에디터 UI는 최종 렌더링 결과 위에 올라가야 하므로 가장 마지막에 그려진다.

    즉 각 Pass는 따로 떨어진 단계가 아니라, 다음 Pass가 사용할 정보를 준비하는 흐름으로 이어져 있다. 이 글에서는 각 Pass의 세부 구현보다 “왜 이런 단계가 필요한지”에 초점을 맞췄다.


    왜 Deferred Rendering을 사용했을까?

    우리 엔진의 렌더링 목표 중 하나는 많은 오브젝트가 있는 씬에서도 프레임을 안정적으로 유지하는 것이었다. 구체적으로는 약 1000개의 오브젝트를 동시에 렌더링하면서 60FPS를 방어하는 것을 목표로 잡았다. 그래서 렌더링 구조를 볼 때도 단순히 “화면에 그려진다”에서 끝나는 것이 아니라, 오브젝트 수와 조명 수가 늘어났을 때 비용이 어떻게 증가하는지를 신경 써야 했다. Forward Rendering은 오브젝트를 그리면서 조명 계산도 함께 수행하는 방식이다. 구조가 단순하다는 장점이 있지만, 오브젝트 수와 동적 광원 수가 늘어나면 각 오브젝트가 영향을 받는 광원에 대해 반복적으로 계산해야 한다.

    우리 엔진은 이 비용을 줄이기 위해 Deferred Rendering을 사용했다. Deferred Rendering은 먼저 Geometry Pass에서 화면에 필요한 정보를 G-Buffer에 저장한다. 위치, 노멀, 알베도, PBR 정보 같은 것들이 여기에 들어간다. 그리고 Lighting Pass에서 이 정보를 읽어 화면 공간에서 조명을 계산한다. 즉 오브젝트를 그리는 단계와 조명을 계산하는 단계를 분리한 것이다.

    이 구조 덕분에 다수의 오브젝트가 있는 씬에서도 조명 계산 흐름을 비교적 예측 가능한 형태로 가져갈 수 있었다. 다만 광원이 무제한으로 늘어나면 Lighting Pass 비용, Shadow Map 비용, 메모리 대역폭 사용량이 함께 증가하기 때문에 동적 광원 수는 4개로 제한했다. 결국 이 프로젝트에서 Deferred Rendering을 선택한 이유는 단순히 더 좋아서가 아니라, 1000개 오브젝트 렌더링과 60FPS 방어라는 목표 안에서 조명 계산 비용을 제어하기 위해서였다.


    Background Pass

    Background Pass는 한 프레임의 가장 뒤쪽을 먼저 준비하는 단계다. AL Engine에서는 HDR 이미지를 CubeMap으로 변환한 뒤, Skybox 배경 이미지로 사용한다.

    이 배경은 단순히 화면 뒤에 깔리는 이미지로만 쓰이지 않는다. Lighting Pass에서 환경광을 계산할 때도 함께 사용된다.

    즉 Background Pass는 “그냥 배경을 그리는 단계”라기보다, 이후 조명 계산에 필요한 환경 정보를 준비하는 단계에 가깝다.


    Shadow Pass

    Shadow Pass는 카메라가 아니라 빛의 시점에서 장면을 다시 보는 단계다. 그림자를 계산하려면 먼저 광원의 시점에서 장면을 한 번 바라봐야 한다. 카메라가 보는 장면이 아니라, 빛이 보는 장면을 렌더링하는 것이다.

    이때 색상은 필요 없고 깊이 정보만 필요하다. 이렇게 얻은 깊이 정보가 Shadow Map이 된다.

    나중에 Lighting Pass에서는 이 Shadow Map을 이용해 특정 픽셀이 빛을 받고 있는지, 아니면 다른 물체에 가려져 그림자 영역에 있는지를 판단한다. Directional Light와 Spot Light는 2D Shadow Map을 사용했고, Point Light는 모든 방향으로 빛을 내기 때문에 Cube Shadow Map을 사용했다.


    Geometry Pass

    Geometry Pass는 Deferred Rendering의 핵심이다. 이 단계에서는 모델을 렌더링하지만, 아직 최종 색상을 만들지는 않는다. 대신 각 픽셀에 대해 다음 정보를 G-Buffer에 저장한다.

    • Position
    • Normal
    • Albedo
    • PBR
    • Depth

    처음에는 “어차피 화면에 색을 보여줄 거면 바로 계산하면 되지 않나?”라는 생각도 했다. 하지만 Deferred Rendering에서는 이 정보를 먼저 모아두는 것이 중요하다. 조명 계산은 다음 Lighting Pass에서 한 번에 처리하기 때문이다.

    Geometry Pass는 쉽게 말하면, 조명을 계산하기 위한 재료를 모아두는 단계다. 여기서 중요한 점이 하나 있다. 정적 모델이든 스켈레탈 애니메이션 모델이든, 결국 Geometry Pass에 들어온다는 점이다.

    다만 스켈레탈 모델은 Vertex Shader에서 Bone Matrix를 이용해 정점 위치를 변형한 뒤 G-Buffer에 기록된다. 이 부분은 다음 글에서 더 자세히 다룰 예정이다.


    Lighting Pass

    Lighting Pass에서는 G-Buffer에 저장된 정보를 읽어 실제 조명을 계산한다. Geometry Pass에서 모아둔 Position, Normal, Albedo, PBR 정보를 가져오고, Shadow Pass에서 만든 Shadow Map도 함께 사용한다. AL Engine에서는 이 단계에서 PBR Lighting을 계산하고, Shadow와 Background를 합성해 최종 HDR 이미지를 만든다.

    즉 우리가 실제로 보는 화면의 색은 대부분 이 단계에서 결정된다. Deferred Rendering을 쓰면 이 구조가 꽤 명확해진다.

    Geometry Pass는 재료를 모으고, Lighting Pass는 그 재료로 최종 이미지를 만든다.


    Collider Pass

    Collider Pass는 게임 실행 화면을 위한 단계라기보다는 에디터를 위한 단계다. 에디터에서 선택한 Entity의 Collider를 와이어프레임으로 보여주기 위해 사용한다.

    실제 게임에서는 보이지 않지만, 개발 중에는 꽤 중요한 기능이었다. Collider가 어디에 있는지 직접 확인할 수 있어야 충돌 문제를 빠르게 찾을 수 있기 때문이다.


    GUI Pass

    마지막은 GUI Pass다. Lighting Pass까지 끝나면 Viewport에 보여줄 최종 이미지가 만들어진다. 그 이미지를 ImGui 화면에 붙이고, 에디터 UI와 함께 SwapChain을 통해 화면에 출력한다.

    즉 우리가 에디터에서 보는 화면은 단순히 렌더링 결과만 있는 것이 아니다. 렌더링된 Viewport 이미지 위에 Hierarchy, Inspector 같은 에디터 UI가 함께 얹혀서 출력된다.

    Background Pass와 Shadow Pass는 이후 렌더링에 필요한 정보를 준비하는 단계다. Geometry Pass는 조명 계산에 필요한 재료를 G-Buffer에 저장하고, Lighting Pass는 그 재료를 바탕으로 최종 이미지를 만든다. Collider Pass와 GUI Pass는 에디터에서 사용할 시각 정보를 덧붙여, 최종적으로 개발자가 보는 화면을 완성한다.


    여기서 하나 더 궁금해지는 지점

    여기까지 보면 한 프레임이 어떻게 만들어지는지는 어느 정도 보인다. 그런데 움직이는 캐릭터는 어디서 처리될까? Geometry Pass에서 모델을 그릴 때 이미 캐릭터는 움직이고 있다.

    그럼 애니메이션은 Geometry Pass 안에서 계산되는 걸까? 처음에는 나도 그렇게 생각했다. 하지만 실제 구조는 조금 다르다.


    그래서 애니메이션은 어디서 계산될까?

    애니메이션은 Geometry Pass에서 처음 계산되는 것이 아니다. CPU에서 먼저 Animation Clip과 State를 기반으로 Pose를 계산한다. 그다음 Skeleton을 따라 최종 Bone Matrix를 만든다.

    이렇게 만들어진 Bone Matrix가 GPU로 전달되고, Geometry Pass의 Vertex Shader에서 GPU Skinning에 사용된다. 즉 Geometry Pass는 애니메이션을 “계산한다”기보다는, 이미 계산된 Bone Matrix를 이용해 정점 위치를 변형하는 단계에 가깝다.


    AL Engine에서는 실제로 이렇게 흘러간다

    조금 더 내부적으로 보면 CPU와 GPU의 역할이 나뉜다. CPU에서는 게임 로직과 애니메이션 상태를 갱신한다. Animation Clip에서 현재 시간에 맞는 키프레임을 찾고, State Machine을 통해 어떤 애니메이션을 재생할지 결정한다.

    그 후 Skeleton 구조를 따라 최종 Bone Matrix를 계산한다. GPU는 이 Bone Matrix를 받아 Vertex Shader에서 정점에 적용한다. 이 과정이 GPU Skinning이다.

    Skinning이 끝난 정점은 일반 모델처럼 Geometry Pass를 지나 G-Buffer에 저장되고, 이후 Lighting Pass에서 최종 이미지로 만들어진다. 이 흐름을 이해하고 나서야 “애니메이션이 렌더링 파이프라인 어디에 들어가는지”가 조금 명확해졌다.

    애니메이션은 렌더링과 완전히 분리된 것도 아니고, 그렇다고 Lighting Pass처럼 후반부에 처리되는 것도 아니다. Geometry Pass에 들어가기 직전, 그리고 Geometry Pass의 Vertex Shader 안에서 렌더링과 만난다.


    마무리

    이번 글을 정리하면, 한 프레임은 단순히 모델을 화면에 그리는 과정이 아니었다. 배경을 준비하고, 빛의 시점에서 그림자 정보를 만들고, 모델의 재질 정보를 G-Buffer에 저장한 뒤, 그 정보를 바탕으로 조명을 계산한다. 그리고 마지막으로 에디터 UI를 얹어 화면에 출력한다. 애니메이션도 이 흐름 바깥에 따로 존재하지 않는다. CPU에서 계산된 Bone Matrix가 GPU로 전달되고, Geometry Pass의 Vertex Shader에서 정점에 적용되면서 렌더링 파이프라인 안으로 들어온다.

    이 흐름을 이해하고 나서야 “캐릭터가 움직이는 화면 한 장”이 실제로 어떤 단계들을 거쳐 만들어지는지 조금 명확해졌다. 이번 글은 전체 지도를 보는 글에 가깝다. 다음 글에서는 이 지도에서 가장 궁금했던 부분인 스켈레탈 애니메이션을 조금 더 깊게 다뤄보려고 한다.

    다음 글: 애니메이션은 언제 움직일까?