FBO 텍스처를 ImGui::Image로 붙이는 뷰포트 패턴

레이아웃에서 3D 그래픽은 OpenGL(또는 Vulkan)이 그리고, 창 장식·도킹· 오버레이는 ImGui가 담당합니다. 이 글에서는 ​이미 준비된 GL 텍스처 ID 를 ImGui::Image로 Viewport 창에 표시하고, ​포커스·마우스 로컬 좌표 를 카메라/피킹으로 넘기는 UI 쪽 패턴을 정리합니다.

완성할 예제

예제는 다음을 연결합니다.

  • Viewport 창 안에 ImGui::Image(tex_id, size)

  • 창이 hovered / focused일 때만 카메라 입력 허용

  • 마우스 위치를 창 로컬 → 정규화(0~1) 좌표로 변환

  • WantCaptureMouse와 뷰포트 입력의 역할 구분

읽고 나면 도킹된 Viewport에 씬을 넣고, UI와 3D 입력을 분리할 수 있습니다.

왜 Image 뷰포트가 표준인가

전체 화면을 GL만으로 그리면 메뉴·패널·로그를 겹치기 어렵습니다. 반대로 ImGui만으로는 대용량 메쉬 렌더링이 맞지 않습니다. ​오프스크린으로 씬을 그린 뒤, 결과 텍스처를 UI 위젯으로 제출 하면 두 세계의 책임이 깔끔해집니다.

데이터 흐름은 단방향이 안전합니다.

  1. GL: FBO에 씬 렌더 → 컬러 텍스처

  2. ImGui: 해당 텍스처를 Image로 표시

  3. 입력: 창 로컬 좌표만 카메라/피커에 전달

최소 Viewport 그리기

GLuint 텍스처를 ImTextureID로 캐스팅하는 방식이 데스크톱 GL 백엔드에서 흔합니다. 백엔드마다 타입이 다를 수 있으니 프로젝트 래퍼를 확인합니다.

Cpp

struct ViewportState { ImTextureID texture = 0; ImVec2 texture_size = ImVec2(1, 1); // FBO 해상도 bool focused = false; bool hovered = false; ImVec2 mouse_uv = ImVec2(-1.0f, -1.0f); // 0~1, 밖이면 -1 }; void DrawViewportWindow(ViewportState& vp) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::Begin("Viewport"); const ImVec2 avail = ImGui::GetContentRegionAvail(); const ImVec2 image_size = avail; // 창에 맞춤 (레터박스 정책은 선택) // 커서 위치 = 이미지 좌상단 (WindowPadding 0 가정) const ImVec2 cursor = ImGui::GetCursorScreenPos(); ImGui::Image(vp.texture, image_size); vp.hovered = ImGui::IsItemHovered(); vp.focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); if (vp.hovered) { const ImVec2 mouse = ImGui::GetIO().MousePos; const float u = (mouse.x - cursor.x) / image_size.x; const float v = (mouse.y - cursor.y) / image_size.y; vp.mouse_uv = ImVec2(u, v); } else { vp.mouse_uv = ImVec2(-1.0f, -1.0f); } ImGui::End(); ImGui::PopStyleVar(); }

v가 위에서 아래로 증가하는 ImGui 좌표와, GL의 하단 원점 텍스처 좌표가 다르면 피킹/UV에서 Y를 뒤집을지 한 곳에서 결정합니다.

입력 라우팅

뷰포트가 포커스되지 않았는데 카메라가 움직이면 UX가 깨집니다. 슬라이더를 드래그하는 동안 궤도 카메라가 돌아가면 안 됩니다.

Cpp

void UpdateCameraFromViewport(const ViewportState& vp, Camera& cam) { if (!vp.hovered || !vp.focused) { return; } ImGuiIO& io = ImGui::GetIO(); // 패널 위 입력은 ImGui가 가져간 경우가 많음 // 뷰포트 Image 위에서는 보통 WantCaptureMouse가 true여도 // “우리가 의도한 3D 입력”으로 처리하는 정책을 택할 수 있음 if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) { cam.orbit(io.MouseDelta.x, io.MouseDelta.y); } if (io.MouseWheel != 0.0f) { cam.dolly(io.MouseWheel); } }

정책은 팀마다 다릅니다. 안전한 기본값은 hovered이고, 다른 모달/팝업이 없을 때만 카메라를 움직이는 것입니다.

HiDPI와 좌표

Retina 등에서 프레임버퍼 스케일이 1이 아닐 수 있습니다. 마우스 스크린 좌표는 ImGui 공간을 쓰고, FBO 픽셀로 바꿀 때만 스케일을 곱합니다.

Cpp

ImVec2 ToFramebufferPixel(const ViewportState& vp, ImVec2 uv) { const ImGuiIO& io = ImGui::GetIO(); const float sx = io.DisplayFramebufferScale.x; const float sy = io.DisplayFramebufferScale.y; // uv는 Image 위 0~1. FBO 해상도가 image_size*scale 일 수 있음 return ImVec2(uv.x * vp.texture_size.x /* * sx 정책에 따라 */, uv.y * vp.texture_size.y /* * sy */); }

핵심은 ​한 좌표 공간만 섞지 말고, “ImGui 포인트”와 “FBO 텍셀”을 함수 경계에서 변환하는 것입니다.

프레임 순서

권장 순서는 다음과 같습니다.

  1. 창 크기 변화 감지 → FBO 리사이즈(OpenGL 쪽)

  2. FBO에 씬 렌더

  3. ImGui::NewFrame

  4. DockSpace + DrawViewportWindow에서 Image

  5. ImGui::Render + ImGui 백엔드 드로우

  6. (선택) 스왑

ImGui가 먼저 텍스처를 참조하기 전에, 해당 프레임의 FBO 렌더가 끝나 있어야 깜빡임이 줄어듭니다.

창 크기와 FBO 요청

UI는 “원하는 픽셀 크기”만 알리면 되고, 실제 할당은 GL 쪽이 담당합니다.

Cpp

struct FboRequest { int width = 1; int height = 1; bool dirty = false; }; void UpdateFboRequestFromViewport(const ImVec2& image_size, FboRequest& req) { const int w = std::max(1, static_cast<int>(image_size.x)); const int h = std::max(1, static_cast<int>(image_size.y)); if (w != req.width || h != req.height) { req.width = w; req.height = h; req.dirty = true; } }

DrawViewportWindow에서 image_size를 얻은 직후 이 함수를 호출하고, 렌더 패스 앞에서 dirty이면 FBO를 리사이즈합니다. 매 프레임 재할당하지 않도록 ​크기가 바뀔 때만 플래그를 세우는 것이 중요합니다.

설계 체크리스트

  1. Viewport는 WindowPadding = 0으로 Image를 꽉 채웁니다.

  2. IsItemHovered / IsWindowFocused로 입력 게이트를 둡니다.

  3. 마우스는 커서 원점 기준 로컬 → UV로 정규화합니다.

  4. Y축 뒤집기 정책을 피킹·셰이더와 한곳에서 공유합니다.

  5. 텍스처 수명(삭제 시점)은 ImGui가 참조를 끝낸 뒤로 미룹니다.

  6. FBO 생성 세부 구현은 OpenGL 모듈에 남기고 UI는 ID·크기만 받습니다.

자주 발생하는 문제

이미지가 뒤집어져 보임

일부 백엔드/샘플러는 UV uv1(1,0)으로 뒤집어 전달합니다. ImGui::Image(tex, size, uv0, uv1)uv1.y를 조정합니다.

검은 화면

텍스처 ID 0, 아직 렌더하지 않은 FBO, 또는 잘못된 유닛 바인딩을 의심합니다. UI보다 먼저 FBO 패스가 돌았는지 확인합니다.

패널 드래그 중에도 카메라가 움직임

hovered만 보고 focused를 빼먹은 경우입니다. 또는 우클릭 드래그 조건을 너무 넓게 잡은 경우입니다.

피킹 좌표가 어긋남

Image 위에 툴바를 SameLine으로 올려 커서 원점이 밀린 경우입니다. 좌표 기준을 GetItemRectMin()으로 통일합니다.

imgui_impl_opengl3 문서의 텍스처 바인딩 규칙을 함께 보면 백엔드별 차이를 놓치지 않습니다.

On this page