세 개의 FBO로 나누는 멀티패스 렌더링

3D 에디터나 메시 뷰어에서 뷰포트에 씬을 그리고, 마우스로 객체를 고르고, ImGui 같은 UI에 텍스처로 붙이려면 ​기본 프레임버퍼 한 장 만으로는 서로의 역할이 뒤섞이거나 관리가 어려워집니다. 이 글에서는 오프스크린 FBO 를 역할별로 나누어 scene / pick / display(blit) 각각 구성하는 패턴을 설명합니다.

/*
참고: 여기서 "blit"은 OpenGL에서 'glBlitFramebuffer'와 같이 한 프레임버퍼의 내용을 GPU 상에서 다른 프레임버퍼로 효율적으로 복사하는 작업을 의미해요.
즉, CPU로 데이터를 전송하지 않고, GPU 메모리 내에서 직접 픽셀 데이터를 복사함으로써 속도와 효율이 뛰어난 방식입니다. */

완성할 예제

다음 세 개의 항목이 한 프레임 안에서 순서대로 돕니다.

  • Scene FBO: 조명·머티리얼이 적용된 컬러 + 깊이

  • Pick FBO: 객체 ID를 RGB로 기록한 컬러

  • Display: scene 컬러를 윈도우 또는 UI 텍스처로 복사(blit)

이 글을 다 읽고 나면, 뷰포트가 리사이즈될 때마다 attachment를 적절하게 새로 만들고, 각 렌더 패스의 경계를 glBindFramebuffer(0) 시점에 명확히 구분하는 미니 프레임워크를 자연스럽게 설계할 수 있게 됩니다.

왜 기본 프레임버퍼만으로는 부족한가

만약 기본 프레임버퍼에 그대로 그리게 되면, 아래와 같은 다양한 용도가 한 공간에 얽히게 되어 관리가 어려워집니다.

  1. UI나 도킹 패널까지 포함된 최종 화면 합성 결과

  2. 오브젝트 피킹을 위한 ID 색상 정보

  3. 이미지 캡처나 후처리용 입력 데이터

이런 각기 다른 관심사는 OpenGL의 주요 기능과 대략 다음처럼 연결됩니다.

  1. 오프스크린 렌더링 — FBO와 texture/renderbuffer attachment 활용

  2. FBO 상태 점검 — glCheckFramebufferStatus로 완전성 확인

  3. 읽기·쓰기 분리 — GL_READ_FRAMEBUFFER, GL_DRAW_FRAMEBUFFER로 구분

  4. 화면에 최종 반영 — glBlitFramebuffer나 fullscreen quad 처리

  5. 해상도 변화 대응 — attachment 재할당 및 viewport 동기화

FBO 생성과 attachment

컬러는 샘플링이 필요하므로 texture, 깊이는 renderbuffer로 두는 예입니다.

Cpp

struct FrameTarget { GLuint fbo = 0; GLuint color_tex = 0; GLuint depth_rbo = 0; int width = 0; int height = 0; }; void createTarget(FrameTarget& t, int w, int h, bool with_depth) { destroyTarget(t); t.width = w; t.height = h; glGenFramebuffers(1, &t.fbo); glBindFramebuffer(GL_FRAMEBUFFER, t.fbo); glGenTextures(1, &t.color_tex); glBindTexture(GL_TEXTURE_2D, t.color_tex); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, t.color_tex, 0); if (with_depth) { glGenRenderbuffers(1, &t.depth_rbo); glBindRenderbuffer(GL_RENDERBUFFER, t.depth_rbo); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, w, h); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, t.depth_rbo); } const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) { // 로그 후 실패 처리 } glBindFramebuffer(GL_FRAMEBUFFER, 0); }

Pick 타깃은 GL_NEAREST 필터와 depth attachment를 유지하되, 셰이더는 flat ID만 출력하도록 둡니다.

한 프레임의 패스 순서

Text

bind Scene FBO → clear → draw meshes / point cloud (shaded) bind Pick FBO → clear → draw same geometry (ID colors) unbind → blit Scene color → default FB or UI texture (필요 시) bind Pick as READ → glReadPixels at mouse

Cpp

void renderFrame(FrameTarget& scene, FrameTarget& pick, int fb_w, int fb_h) { // 1) Scene glBindFramebuffer(GL_FRAMEBUFFER, scene.fbo); glViewport(0, 0, scene.width, scene.height); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawScene(/* pick_mode = */ false); // 2) Pick glBindFramebuffer(GL_FRAMEBUFFER, pick.fbo); glViewport(0, 0, pick.width, pick.height); glDisable(GL_BLEND); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); drawScene(/* pick_mode = */ true); // 3) Display blit → 기본 프레임버퍼(또는 별도 display FBO) glBindFramebuffer(GL_READ_FRAMEBUFFER, scene.fbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBlitFramebuffer(0, 0, scene.width, scene.height, 0, 0, fb_w, fb_h, GL_COLOR_BUFFER_BIT, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, 0); }

리사이즈와 viewport

윈도우·독 패널 크기가 바뀌면 attachment 해상도를 맞춥니다. width/height가 같으면 재생성하지 않아도 됩니다.

Cpp

void ensureSize(FrameTarget& t, int w, int h, bool with_depth) { if (w <= 0 || h <= 0) { return; } if (t.width == w && t.height == h && t.fbo != 0) { return; } createTarget(t, w, h, with_depth); }

HiDPI에서는 ​윈도우 논리 크기 와 framebuffer 픽셀 크기 가 다를 수 있습니다.
여기서 "blit 목적지"란, glBlitFramebuffer로 화면에 복사될 때의 ​최종 프레임버퍼(또는 디스플레이 FBO) 의 크기와 좌표 체계를 의미합니다.
마우스 좌표 변환이나 후속 연산에서도 혼동을 막으려면, scene FBO에서 display로 blit할 때 목적지 사각형(픽셀 단위)이 어디인지를 프로젝트 전체에서 일관되게 정의해야 합니다.
보통은 프레임버퍼 픽셀(즉, 실제 GL framebuffer 해상도)이 기준이며,
마우스 좌표 역시 같은 기준(디스플레이 픽셀/프레임버퍼 픽셀)에서 해석해 사용하는 것이 버그를 줄이는 방법입니다.

설계 체크리스트

  1. Scene / Pick / Display의 역할과 바인드 순서를 이름과 함께 정리해 둡니다.

  2. FBO를 생성한 직후에는 항상 glCheckFramebufferStatus로 상태를 확인합니다.

  3. 각 패스마다 glViewport와 clear 마스크를 명확하게 지정합니다.

  4. Pick 패스에서는 blending, multisampling, tone mapping을 모두 비활성화합니다.

  5. 리사이즈가 발생할 때 color와 depth를 모두 다시 할당합니다.

  6. UI에 전달하는 텍스처는 반드시 "완성된 scene 컬러 텍스처"만 노출해야 합니다.

자주 발생하는 문제

FBO가 incomplete

attachment 크기 불일치, depth 포맷 미지원, color attachment를 하나도 안 붙인 경우를 의합니다. depth-only 타깃이 아니라면 GL_COLOR_ATTACHMENT0이 있는지 봅니다.

화면은 그리는데 UI 텍스처가 검정

blit/glBindTexture 전에 scene FBO를 unbind했는지, UI가 샘플하는 유닛에 다른 텍스처가 묶여 있는지 확인합니다.

피킹 좌표가 어긋남

Y축 반전, 독 패널 offset, HiDPI scale을 pick FBO 해상도와 맞춰야 합니다. scene과 pick의 width/height가 다르면 좌표를 스케일합니다.

리사이즈 후 깨지거나 비율이 이상함

attachment는 새 크기인데 projection의 aspect만 예전 값이거나, 그 반대인 경우가 많습니다. FBO 크기 변경과 카메라 aspect를 같은 이벤트에서 갱신합니다.

On this page