ModernGL奮闘記 (12) - ImGui -
ImGuiをModernGLと組み合わせて使いたいと思い実装してみました.
これまでの取り組み
インストール
公式にある通り,ImGuiのPython Bindingには色々あるようです.今回は紹介されている中で一番上に紹介されていたという理由だけでpyimguiを使ってみました.
pip install imgui[full]
[full]にあるようなオプションについてはこのページを参照.
コード(改訂前)
色々なドキュメントと実装をネット上で探りながら作ってみました.from glfw_quad import Quadで読み込んでいるQuadクラスに関しては,以下の過去記事に置いてあります.
https://mugichoko.hatenablog.com/entry/2021/11/13/235559mugichoko.hatenablog.com
所感としては,細かなところで色々詰まったなと... ModernGLは使うWindowによって,なんだかんだ書き方が結構変わってくるみたいですね.
今回もこれまでと同じようにGLFWを使っているのですが,ModernGL-Windowに用意されているmoderngl_window.context.glfw.window.Windowクラスを使いました.これは,これまでのmoderngl_window.create_window_from_settings()を使ってWindowを作成する方法だと,マウススクロールのCallback関数の実装ができなさそうだったためです.
結果,resourceへのアクセスの仕方やFBOの用意の仕方を少し変更しなければなりませんでした.
追記 (20 Jan., 2022):盛大な勘違いしていました.普通に過去記事でもマウススクロールのCallback関数を実装できていました.
import glfw import imgui import moderngl as mgl import numpy as np from pathlib import Path from moderngl_window.context.glfw.window import Window from moderngl_window.scene.camera import Camera from imgui.integrations.glfw import GlfwRenderer from glfw_quad import Quad # https://github.com/moderngl/moderngl-window/blob/dc16c9c0ea9e95b244056dd6b2adf09cb36e5fbe/moderngl_window/context/glfw/window.py class App(Window): def __init__(self, **kwargs): # Input parameters # Ref: https://moderngl-window.readthedocs.io/en/latest/reference/context/basewindow.html?highlight=BaseWindow super().__init__(**kwargs) # ImGui imgui.create_context() self.impl = GlfwRenderer(self._window, attach_callbacks=False) # UI elements self._slider_value = 0 # additional callback glfw.set_scroll_callback(self._window, self.glfw_mouse_scroll_callback) # Resources self.resource_dir = Path(__file__).parent.resolve() / "resources" self.shader_dir = (self.resource_dir / "shaders").resolve() self.program = self.ctx.program( vertex_shader=open(self.shader_dir / "uv_vs.glsl").read(), fragment_shader=open(self.shader_dir / "uv_fs.glsl").read() ) # Quad self.quad = Quad(self.ctx, self.program) # FBO for rendering Quad self.fbo_quad = self.ctx.framebuffer( color_attachments=self.ctx.texture((256, 256), 4), depth_attachment=self.ctx.depth_texture((256, 256)), ) # Camera self.camera = Camera(aspect_ratio=self.fbo_quad.width / self.fbo_quad.height, near=0.01, far=100.0) self.camera.set_position(0, 0.0, 1.5) self.camera.look_at(pos=(0, 0, 0)) def glfw_mouse_scroll_callback(self, window, x_offset: float, y_offset: float): self._slider_value = max(0, min(self._slider_value + y_offset, 100)) self._mouse_scroll_event_func(x_offset, y_offset) def render_quad(self): self.fbo_quad.use() self.fbo_quad.clear(1, 0, 1, 1) self.ctx.enable(mgl.DEPTH_TEST | mgl.CULL_FACE) self.quad.render(self.camera) self.fbo.use() # get back to the default target def render_ui(self): glfw.poll_events() self.impl.process_inputs() imgui.new_frame() if imgui.begin_main_menu_bar(): if imgui.begin_menu("File", True): clicked_quite, selected_quit = imgui.menu_item( "Quit", "Esc", False, True ) if clicked_quite: self.close() imgui.end_menu() imgui.end_main_menu_bar() imgui.begin("UI info.", True) changed, self._slider_value = imgui.slider_int( "slider", self._slider_value, min_value=0, max_value=100 ) screen_pos = imgui.get_cursor_screen_pos() # this gets the location where it's called # https://github.com/moderngl/moderngl-window/blob/268c8d886e99e9aae05ea3bb5941994dc2a99569/examples/integration_imgui_image.py#L79 imgui.image(self.fbo_quad.color_attachments[0].glo, *self.fbo_quad.size) if imgui.is_item_hovered(): imgui.begin_tooltip() imgui.text("Runtime rendering!") imgui.end_tooltip() imgui.text(f"mouse pos: {self._mouse_pos}") imgui.text(f"screen pos: ({screen_pos[0]}, {screen_pos[1]})") relative_mouse_pos = (np.array(self._mouse_pos) - np.array(screen_pos)).astype(int) imgui.text(f"mouse pos in image: ({relative_mouse_pos[0]}, {relative_mouse_pos[1]})") imgui.end() imgui.begin("Desc.", True) imgui.push_text_wrap_pos(imgui.get_window_width()) imgui.text_colored("slider:", 1, 1, 0) imgui.text("An example slider (int).") imgui.text_colored("image:", 1, 1, 0) imgui.text("An example image rendered at runtime.") imgui.text_colored("mouse pos:", 1, 1, 0) imgui.text("Mouse position in the window coordinate system.") imgui.text_colored("screen pos", 1, 1, 0) imgui.text("UI window location.") imgui.text_colored("mouse pos in image", 1, 1, 0) imgui.text("Mouse position in the image coordinate system.") imgui.pop_text_wrap_pos() imgui.end() imgui.render() self.impl.render(imgui.get_draw_data()) if __name__ == "__main__": app = App(size=(640, 480), title="ImGui test") while not app.is_closing: app.clear(1, 1, 0, 0) app.render_quad() app.render_ui() app.swap_buffers()
コード(改訂後:後日追記分)
追記 (20 Jan., 2022):先述の通り,これまでの書き方を踏襲して同じことができました.
# # glfw_base_window.py # from pathlib import Path import moderngl_window as mglw from moderngl_window.conf import settings from moderngl_window import resources class BaseWindow: def __init__(self, wnd_size=(512, 512), title="GLFW") -> None: # create a gl window: https://moderngl-window.readthedocs.io/en/latest/reference/settings.conf.settings.html#moderngl_window.conf.Settings.WINDOW settings.WINDOW["class"] = "moderngl_window.context.glfw.Window" # OpenGL 4.3 or upper is required for compute shaders settings.WINDOW["gl_version"] = (4, 3) settings.WINDOW["title"] = title settings.WINDOW["size"] = wnd_size settings.WINDOW["aspect_ratio"] = wnd_size[0] / wnd_size[1] self.wnd = mglw.create_window_from_settings() # Resources self.resource_dir = Path(__file__).parent.resolve() / "resources" self.shaders_dir = (self.resource_dir / "shaders").resolve() self.textures_dir = (self.resource_dir / "textures").resolve() resources.register_scene_dir((self.resource_dir / "models").resolve()) resources.register_program_dir(self.shaders_dir) # Set mouse callback ## Ref: https://github.com/moderngl/moderngl-window/blob/master/examples/custom_config_class.py self.wnd.resize_func = self.resize self.wnd.key_event_func = self.key_event self.wnd.mouse_position_event_func = self.mouse_position_event self.wnd.mouse_scroll_event_func = self.mouse_scroll_event self.wnd.mouse_drag_event_func = self.mouse_drag_event self.wnd.mouse_press_event_func = self.mouse_press_event self.wnd.mouse_release_event_func = self.mouse_release_event self.wnd.unicode_char_entered_func = self.unicode_char_entered ##self.wnd.mouse_exclusivity = True self._mouse_pos = [0, 0] self._mouse_pos_delta = [0, 0] self._scroll_offset = [0, 0] def resize(self, width: int, height: int): pass def key_event(self, key, action, modifiers): pass def mouse_position_event(self, x, y, dx, dy) -> None: pass def mouse_scroll_event(self, x_offset, y_offset) -> None: pass def mouse_drag_event(self, x, y, dx, dy): pass def mouse_press_event(self, x, y, button): pass def mouse_release_event(self, x: int, y: int, button: int): pass def unicode_char_entered(self, char): pass
# # glfw_imgui.py # import numpy as np import imgui import moderngl as mgl from moderngl_window.integrations.imgui import ModernglWindowRenderer from moderngl_window.resources import programs from moderngl_window.meta import ProgramDescription from moderngl_window.scene.camera import Camera from glfw_base_window import BaseWindow from glfw_quad import Quad # https://github.com/moderngl/moderngl-window/blob/2.1/examples/integration_imgui.py class App(BaseWindow): def __init__(self, wnd_size: tuple[int, int], title: str = "App") -> None: super().__init__(wnd_size=wnd_size, title=title) imgui.create_context() self.wnd.ctx.error self.imgui = ModernglWindowRenderer(self.wnd) # Shaders self.program = programs.load( ProgramDescription( vertex_shader="uv_vs.glsl", fragment_shader="uv_fs.glsl", ) ) # Quad self.quad = Quad(self.wnd.ctx, self.program) # FBO for rendering Quad self.fbo_quad = self.wnd.ctx.framebuffer( color_attachments=self.wnd.ctx.texture((256, 256), 4), depth_attachment=self.wnd.ctx.depth_texture((256, 256)), ) # Register the texture so that imgui can use it # https://github.com/moderngl/moderngl-window/blob/268c8d886e99e9aae05ea3bb5941994dc2a99569/examples/integration_imgui_image.py#L79 self.imgui.register_texture(self.fbo_quad.color_attachments[0]) # Camera self.camera = Camera(aspect_ratio=self.fbo_quad.width / self.fbo_quad.height, near=0.01, far=100.0) self.camera.set_position(0, 0.0, 1.5) self.camera.look_at(pos=(0, 0, 0)) def resize(self, width: int, height: int): self.imgui.wnd.fixed_aspect_ratio = width / height self.imgui.wnd.set_default_viewport() self.imgui.resize(width, height) def key_event(self, key, action, modifiers): self.imgui.key_event(key, action, modifiers) def mouse_position_event(self, x, y, dx, dy) -> None: # print("Mouse position pos={} {} delta={} {}".format(x, y, dx, dy)) self._mouse_pos = [x, y] self._mouse_pos_delta = [dx, dy] self.imgui.mouse_position_event(x, y, dx, dy) def mouse_scroll_event(self, x_offset, y_offset) -> None: # print("mouse_scroll_event", x_offset, y_offset) self._scroll_offset = [self._scroll_offset[0] + x_offset, self._scroll_offset[1] + y_offset] self._scroll_offset_delta = [x_offset, y_offset] self.imgui.mouse_scroll_event(x_offset, y_offset) def mouse_drag_event(self, x, y, dx, dy): self.imgui.mouse_drag_event(x, y, dx, dy) def mouse_press_event(self, x, y, button): self.imgui.mouse_press_event(x, y, button) def mouse_release_event(self, x: int, y: int, button: int): self.imgui.mouse_release_event(x, y, button) def unicode_char_entered(self, char): self.imgui.unicode_char_entered(char) def render_quad(self): self.fbo_quad.use() self.fbo_quad.clear(1, 0, 1, 1) self.wnd.ctx.enable(mgl.DEPTH_TEST | mgl.CULL_FACE) self.quad.render(self.camera) self.wnd.use() # get back to the default target def render_ui(self): imgui.new_frame() if imgui.begin_main_menu_bar(): if imgui.begin_menu("File", True): clicked_quite, selected_quit = imgui.menu_item( "Quit", "Esc", False, True ) if clicked_quite: self.wnd.close() imgui.end_menu() imgui.end_main_menu_bar() imgui.begin("UI info.", True) self._scroll_offset[1] = int(max(0, min(self._scroll_offset[1], 100))) changed, self._scroll_offset[1] = imgui.slider_int( "slider", self._scroll_offset[1], min_value=0, max_value=100 ) screen_pos = imgui.get_cursor_screen_pos() # this gets the location where it's called imgui.image(self.fbo_quad.color_attachments[0].glo, *self.fbo_quad.size) if imgui.is_item_hovered(): imgui.begin_tooltip() imgui.text("Runtime rendering!") imgui.end_tooltip() imgui.text(f"mouse pos: {self._mouse_pos}") imgui.text(f"screen pos: ({screen_pos[0]}, {screen_pos[1]})") relative_mouse_pos = (np.array(self._mouse_pos) - np.array(screen_pos)).astype(int) imgui.text(f"mouse pos in image: ({relative_mouse_pos[0]}, {relative_mouse_pos[1]})") imgui.end() imgui.begin("Desc.", True) imgui.push_text_wrap_pos(imgui.get_window_width()) imgui.text_colored("slider:", 1, 1, 0) imgui.text("An example slider (int).") imgui.text_colored("image:", 1, 1, 0) imgui.text("An example image rendered at runtime.") imgui.text_colored("mouse pos:", 1, 1, 0) imgui.text("Mouse position in the window coordinate system.") imgui.text_colored("screen pos", 1, 1, 0) imgui.text("UI window location.") imgui.text_colored("mouse pos in image", 1, 1, 0) imgui.text("Mouse position in the image coordinate system.") imgui.pop_text_wrap_pos() imgui.end() imgui.render() self.imgui.render(imgui.get_draw_data()) def run(self): while not self.wnd.is_closing: self.wnd.clear(1, 1, 0, 0) self.render_quad() self.render_ui() self.wnd.swap_buffers() if __name__ == "__main__": app = App(wnd_size=(640, 480), title="Test ImGui") app.run()
実装結果
以下の映像のようなものが実装できました.小さなこだわりとして,
- スライダがマウススクロールでも操作でる
- 右の
Desc.内のテキストはそのウィンドウのサイズによって折り返される - 画像上にマウスカーソルを被せるとヒントの小窓が出てくる
というのがあります.