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 core나 core 키워드를 지원하지 않기 때문입니다. 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 필수 구조 및 주요 포인트
캔버스 요소 필수
<canvas id="canvas" tabindex="0"></canvas> 처럼 ID가 반드시 canvas인 <canvas> 태그가 HTML에 존재해야 합니다.
이 캔버스는 실제로 OpenGL(ES3)의 렌더 타겟이 되며, ImGui 및 GLFW 입력 처리의 기본 대상이 됩니다.
필요하면 style="width: 100vw; height: 100vh;" 등으로 사이즈 지정도 가능합니다.
Emscripten에서 캔버스 DomElement 지정
Emscripten 런타임이 생성하는 Module 객체 는 내부적으로 Module.canvas라는 속성을 참조해 렌더링 타겟을 찾습니다.
Module.canvas는 위에서 만든 <canvas id="canvas"> DOM 요소와 매칭되어야 하므로, id 값이 꼭 일치해야 하며, shell.html에는 다른 id 값을 쓰지 않도록 주의해야 합니다.
코드 삽입 위치: {{{ 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
설계 체크리스트
웹 GL 헤더는 <GLES3/gl3.h>(또는 동등 경로)로 통일합니다.
GLFW 힌트를 GLFW_OPENGL_ES_API + 3.0으로 맞추고, ImGui에 #version 300 es를 넘깁니다.
InstallEmscriptenCallbacks와 shell의 canvas id를 일치시킵니다.
메인 루프는 emscripten_set_main_loop(스텁)로 감쌉니다.
셰이더는 #version 300 es + precision을 사용합니다.
CMake에 contrib.glfw3, FULL_ES3, shell-file을 모읍니다.
산출물은 HTTP로 서빙하고, 가능하면 io.IniFilename = nullptr로 파일 의존을 줄입니다.
자주 발생하는 문제
emcmake / contrib.glfw3를 못 찾음
emsdk 환경이 셸에 없습니다. source ~/emsdk/emsdk_env.sh 후 emcc -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과 브라우저 루프에 올립니다.
참고