ImGui·Emscripten으로 WebGL2 앱 만들기

브라우저에서 OpenGL 계열 앱을 돌리려면 데스크톱과 다른 ​컨텍스트·헤더·메인 루프·빌드 가 필요합니다. 이 글에서는 ImGui 로 UI를 유지한 채 Emscripten 으로 컴파일해 WebGL2 / OpenGL ES 3 경로로 올리는 방법을 정리합니다.

이 글을 읽고 나면

  • GLFW(창·입력) · ImGui(UI) · Emscripten(Wasm/WebGL)의 역할을 구분할 수 있습니다.

  • __EMSCRIPTEN__ 분기로 ES 컨텍스트 힌트·메인 루프·GLSL version을 맞출 수 있습니다.

  • emcmake + contrib.glfw3 + shell.html.html / .js / .wasm을 만들고 HTTP로 실행하는 흐름을 이해합니다.

특정 렌더 기법(메시, FBO, 파티클 등) 구현은 본 글에서는 다루지 않으며, 그 위에 올릴 ​웹 앱 셸과 빌드 구성 을 다룹니다.

구성 요소

구성 요소

역할

웹에서의 형태

GLFW

창·입력·버퍼 스왑

--use-port=contrib.glfw3

ImGui

즉시 모드 UI

동일 소스 + Emscripten 입력 콜백

Emscripten

C++ → Wasm/JS, 브라우저 루프

emcc / emcmake

WebGL2 / ES 3

GPU API

<GLES3/gl3.h>, #version 300 es

Text

┌─────────────────────────┐ │ 앱 로직 (씬 + ImGui) │ └────────────┬────────────┘ │ ┌───────────┴───────────┐ ▼ ▼ Desktop (참고) Browser GLFW + OpenGL contrib.glfw3 while 루프 + WebGL2 / ES 3 emscripten_set_main_loop

ImGui 자체는 플랫폼에 거의 중립적입니다. 웹에서 바뀌는 것은 GL 백엔드가 ES3로 동작한다는 점, GLSL version 문자열, ​캔버스 입력 연결, ​메인 루프 스케줄 입니다.

ES 컨텍스트와 ImGui 백엔드

창을 만들기 전에 ES API를 요청합니다.

Cpp

#if defined(__EMSCRIPTEN__) const char* glsl_version = "#version 300 es"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); #endif GLFWwindow* window = glfwCreateWindow(1280, 720, "My App", nullptr, nullptr); glfwMakeContextCurrent(window); ImGui_ImplGlfw_InitForOpenGL(window, true); #ifdef __EMSCRIPTEN__ ImGui_ImplGlfw_InstallEmscriptenCallbacks(window, "#canvas"); #endif ImGui_ImplOpenGL3_Init(glsl_version);
  • CMake에서 웹 타겟에 IMGUI_IMPL_OPENGL_ES3를 정의합니다.

Cmake

add_library(imgui_gl STATIC ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp ${imgui_SOURCE_DIR}/imgui_widgets.cpp ${imgui_SOURCE_DIR}/backends/imgui_impl_glfw.cpp ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp ) ... if(EMSCRIPTEN) target_compile_definitions(imgui_gl PUBLIC IMGUI_IMPL_OPENGL_ES3) ... endif()
  • "#canvas"는 HTML 셸의 <canvas id="canvas">와 같아야 합니다.

Cpp

#ifdef __EMSCRIPTEN__ ImGui_ImplGlfw_InstallEmscriptenCallbacks(window, "#canvas"); #endif

GL 심볼은 웹에서 GLES3 헤더로 가져옵니다.

Cpp

#if defined(__EMSCRIPTEN__) #include <GLES3/gl3.h> #endif

브라우저 메인 루프

브라우저 탭에서는 while (true)로 CPU를 붙잡으면 화면이 멈춥니다. 프레임 내용만 공유하고, 호출 스케줄은 emscripten_set_main_loop에 맡깁니다.

Cpp

#ifdef __EMSCRIPTEN__ EMSCRIPTEN_MAINLOOP_BEGIN #else while (!glfwWindowShouldClose(window)) #endif { glfwPollEvents(); // 씬 업데이트 · glClear / draw · ImGui NewFrame → Render glfwSwapBuffers(window); } #ifdef __EMSCRIPTEN__ EMSCRIPTEN_MAINLOOP_END; #endif

프레임 안에서는 보통 ​씬을 default framebuffer에 그린 뒤 ImGui를 올립니다. FBO에 그려 ImGui::Image로 붙이는 구성도 가능하지만, 웹/데스크톱 공통으로 “GL 패스 → UI 패스” 순서만 지키면 상태 오염을 줄일 수 있습니다.

셰이더: #version 300 es

항목

웹 (WebGL2 / ES 3)

버전 지시어

#version 300 es

precision

precision mediump float; 등 (FS에서 특히)

입출력

in / out (ES3 / WebGL2)

데스크톱용 #version 330 core 문자열을 WebGL2 환경에 그대로 넣으면 컴파일 에러가 발생합니다. 왜냐하면 WebGL2(즉, OpenGL ES 3.0)는 GLSL ES 3.00을 기반으로 하여, #version 330 corecore 키워드를 지원하지 않기 때문입니다. WebGL2에서는 반드시 #version 300 es로 시작해야 하며, 데스크톱 GLSL과 미묘하게 다른 예약어 및 내장 함수, 그리고 precision 선언 등이 필요합니다. 따라서 유니폼 및 attribute 레이아웃은 동일하게 유지하더라도, 셰이더 소스 코드 상단의 버전 문자열(및 약간의 문법 차이)을 환경에 맞춰 조건부로 분기해주는 것이 일반적인 해결 방법입니다.

예를 들어, 데스크톱(OpenGL) 환경에서는 아래와 같이 glEnable(GL_DEPTH_TEST), glEnable(GL_CULL_FACE) 등 다양한 렌더링 상태(enable)를 명시적으로 활성화하는 것이 일반적입니다.

Cpp

glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); // ... (기타 데스크톱용 GL 상태 등)

하지만 이러한 상태 중 일부는 WebGL2/ES3에서는 미지원이거나, 기본값이 다르거나, 상황에 따라 불필요할 수 있습니다. 때문에 웹 빌드(emscripten)에서는 해당 코드 블록을 #ifdef __EMSCRIPTEN__ 또는 #ifndef __EMSCRIPTEN__로 감싸 환경별로 구분 처리하는 것이 안전합니다. 예시:

Cpp

#ifndef __EMSCRIPTEN__ // 데스크톱(OpenGL)용 전용 상태 enable glEnable(GL_PROGRAM_POINT_SIZE); // WebGL2에서는 자동 처리됨 #endif

이처럼 상태 enable 코드를 환경에 맞게 분기하면, 호환성 문제나 불필요한 오류를 예방할 수 있습니다.

CMake와 Emscripten 플래그

emcmake cmake로 구성하면 EMSCRIPTEN이 정의됩니다.

Cmake

if(EMSCRIPTEN) target_compile_definitions(imgui_lib PUBLIC IMGUI_IMPL_OPENGL_ES3) target_compile_definitions(my_app PRIVATE IMGUI_DISABLE_FILE_FUNCTIONS) target_compile_options(my_app PRIVATE -sDISABLE_EXCEPTION_CATCHING=1 --use-port=contrib.glfw3) target_link_options(my_app PRIVATE -sDISABLE_EXCEPTION_CATCHING=1 --use-port=contrib.glfw3 -sWASM=1 -sALLOW_MEMORY_GROWTH=1 -sASSERTIONS=1 -sNO_FILESYSTEM=1 -sFULL_ES3=1 --shell-file=${CMAKE_CURRENT_SOURCE_DIR}/shell.html) set_target_properties(my_app PROPERTIES SUFFIX ".html") endif()

옵션

역할

--use-port=contrib.glfw3

브라우저용 GLFW 구현 (ImGui가 권장하는 경로, 구 -sUSE_GLFW=3 대체)

-sFULL_ES3

ES3 계열 API를 웹에서 넓게 쓰기 위한 링크 옵션

-sALLOW_MEMORY_GROWTH

Wasm 힙 성장 허용

-sNO_FILESYSTEM

데모에서 가상 FS 생략 (ini 등 파일 의존도 보통 끔)

--shell-file

캔버스와 Module 훅이 있는 HTML 템플릿

ImGui는 FetchContent 등으로 받아 imgui 본체와 imgui_impl_glfw / imgui_impl_opengl3만 링크하면 됩니다.

shell.html과 실행

ImGui 기반 WebGL(Emscripten) 프로젝트에서 사용하는 shell.html 파일은 단순한 HTML 템플릿 그 이상입니다. 앱이 브라우저 캔버스와 제대로 연동되고, Emscripten이 Wasm/JS 번들로 변환한 코드를 주입할 수 있도록 하기 위해 ​다음과 같은 구조와 역할 이 충족되어야 합니다.

shell.html 필수 구조 및 주요 포인트

  1. ​캔버스 요소 필수

    • <canvas id="canvas" tabindex="0"></canvas> 처럼 ID가 반드시 canvas<canvas> 태그가 HTML에 존재해야 합니다.

    • 이 캔버스는 실제로 OpenGL(ES3)의 렌더 타겟이 되며, ImGui 및 GLFW 입력 처리의 기본 대상이 됩니다.

    • 필요하면 style="width: 100vw; height: 100vh;" 등으로 사이즈 지정도 가능합니다.

  2. Emscripten에서 캔버스 DomElement 지정

    • Emscripten 런타임이 생성하는 Module 객체 는 내부적으로 Module.canvas라는 속성을 참조해 렌더링 타겟을 찾습니다.

    • Module.canvas는 위에서 만든 <canvas id="canvas"> DOM 요소와 매칭되어야 하므로, id 값이 꼭 일치해야 하며, shell.html에는 다른 id 값을 쓰지 않도록 주의해야 합니다.

  3. ​코드 삽입 위치: {{{ SCRIPT }}}

    • Emscripten 빌드 시 Wasm/JS 번들 파일이 자동으로 삽입될 위치를 <!-- {{{ SCRIPT }}} -->와 같이 명시적으로 넣어야 합니다.

    • 이 마커가 없다면 런타임에 JS/Wasm 바인딩이 제대로 주입되지 않고 앱이 실행되지 않습니다.

shell.html 예시 스니펫

Html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>ImGui WebGL2 Demo</title> <style> html, body { height: 100%; margin: 0; overflow: hidden; } canvas { width: 100vw; height: 100vh; display: block; } </style> </head> <body> <canvas id="canvas" tabindex="0"></canvas> <!-- 앱 번들되는 JS/Wasm 코드가 이 자리에 들어옴 --> {{{ SCRIPT }}} </body> </html>

참고로, tabindex="0"은 키보드 입력 처리를 위해 권장되며, 다른 부가적인 HTML 요소(버튼 등)를 추가해도 무방하지만, id="canvas" 캔버스가 반드시 존재해야 합니다.
만약 다른 id를 쓰고 싶다면, C++단과 JS glue code 모두 수정해야 하므로 권장하지 않습니다.

이처럼, shell.html 템플릿이 충족해야 할 최소 요건과 각 조건의 의미를 숙지하고 있으면, 빌드·실행 시 각종 연동 문제와 렌더링 오류를 초기에 막을 수 있습니다.

산출물 예:

Text

index.html my_app.js my_app.wasm

Bash

source ~/emsdk/emsdk_env.sh emcmake cmake -S . -B build-web -DCMAKE_BUILD_TYPE=Release cmake --build build-web python3 -m http.server 8000 --directory build-web

설계 체크리스트

  1. 웹 GL 헤더는 <GLES3/gl3.h>(또는 동등 경로)로 통일합니다.

  2. GLFW 힌트를 GLFW_OPENGL_ES_API + 3.0으로 맞추고, ImGui에 #version 300 es를 넘깁니다.

  3. InstallEmscriptenCallbacks와 shell의 canvas id를 일치시킵니다.

  4. 메인 루프는 emscripten_set_main_loop(스텁)로 감쌉니다.

  5. 셰이더는 #version 300 es + precision을 사용합니다.

  6. CMake에 contrib.glfw3, FULL_ES3, shell-file을 모읍니다.

  7. 산출물은 HTTP로 서빙하고, 가능하면 io.IniFilename = nullptr로 파일 의존을 줄입니다.

자주 발생하는 문제

emcmake / contrib.glfw3를 못 찾음

emsdk 환경이 셸에 없습니다. source ~/emsdk/emsdk_env.shemcc -v로 확인하세요. contrib.glfw3 포트는 Emscripten 3.1.55+에서 권장됩니다.

화면이 검고 셰이더 로그에 version 오류

데스크톱용 #version 330을 웹에 그대로 넣은 경우가 많습니다. #version 300 es와 FS precision을 확인하세요.

ImGui는 보이는데 입력이 이상함

ImGui_ImplGlfw_InstallEmscriptenCallbacks 누락, 또는 canvas id 불일치입니다. 페이지/캔버스 포커스도 확인합니다.

file://에서 무한 로딩

Wasm 로딩 제약입니다. python3 -m http.server로 디렉터리를 여세요.

데스크톱과 웹에서만 깨지는 GL 호출

ES/WebGL에 없는 심볼·enum을 데스크톱 코드에서 그대로 호출한 경우입니다. 웹 빌드(ASSERTIONS) 로그로 먼저 가려내세요.

정리

  • 웹용 OpenGL 앱의 GPU 경로는 WebGL2 / OpenGL ES 3 입니다.

  • ImGui 로 UI를 유지하고, Emscripten 으로 Wasm과 브라우저 루프에 올립니다.


​참고

On this page