From 67a30750e595de058cca57d9c3676a772157d194 Mon Sep 17 00:00:00 2001 From: Jonte Date: Mon, 7 Oct 2024 19:32:57 +0200 Subject: [PATCH 1/3] Add imgui_bundle integration The integration is more or less the exact same as with pyimgui, however since pyimgui is manually coded and imgui_bundle is automated, imgui_bundle has been able to keep up with Dear Imgui's updates. --- moderngl_window/integrations/imgui_bundle.py | 317 +++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 moderngl_window/integrations/imgui_bundle.py diff --git a/moderngl_window/integrations/imgui_bundle.py b/moderngl_window/integrations/imgui_bundle.py new file mode 100644 index 0000000..13f039f --- /dev/null +++ b/moderngl_window/integrations/imgui_bundle.py @@ -0,0 +1,317 @@ +import ctypes + +from imgui_bundle import imgui +from imgui_bundle.python_backends import compute_fb_scale +import moderngl + + +class ModernglWindowMixin: + def resize(self, width: int, height: int): + self.io.display_size = self.wnd.size + self.io.display_framebuffer_scale = compute_fb_scale(self.wnd.size, self.wnd.buffer_size) + + def key_event(self, key, action, modifiers): + keys = self.wnd.keys + + if key in self.REVERSE_KEYMAP: + down = action == keys.ACTION_PRESS + self.io.add_key_event(self.REVERSE_KEYMAP[key], down=down) + + def _mouse_pos_viewport(self, x, y): + """Make sure mouse coordinates are correct with black borders""" + return ( + int( + x + - (self.wnd.width - self.wnd.viewport_width / self.wnd.pixel_ratio) / 2 + ), + int( + y + - (self.wnd.height - self.wnd.viewport_height / self.wnd.pixel_ratio) + / 2 + ), + ) + + def mouse_position_event(self, x, y, dx, dy): + self.io.mouse_pos = self._mouse_pos_viewport(x, y) + + def mouse_drag_event(self, x, y, dx, dy): + self.io.mouse_pos = self._mouse_pos_viewport(x, y) + + if self.wnd.mouse_states.left: + self.io.mouse_down[0] = 1 + + if self.wnd.mouse_states.middle: + self.io.mouse_down[2] = 1 + + if self.wnd.mouse_states.right: + self.io.mouse_down[1] = 1 + + def mouse_scroll_event(self, x_offset, y_offset): + self.io.mouse_wheel = y_offset + + def mouse_press_event(self, x, y, button): + self.io.mouse_pos = self._mouse_pos_viewport(x, y) + + if button == self.wnd.mouse.left: + self.io.mouse_down[0] = 1 + + if button == self.wnd.mouse.middle: + self.io.mouse_down[2] = 1 + + if button == self.wnd.mouse.right: + self.io.mouse_down[1] = 1 + + def mouse_release_event(self, x: int, y: int, button: int): + self.io.mouse_pos = self._mouse_pos_viewport(x, y) + + if button == self.wnd.mouse.left: + self.io.mouse_down[0] = 0 + + if button == self.wnd.mouse.middle: + self.io.mouse_down[2] = 0 + + if button == self.wnd.mouse.right: + self.io.mouse_down[1] = 0 + + def unicode_char_entered(self, char): + io = imgui.get_io() + io.add_input_character(ord(char)) + + +class BaseOpenGLRenderer(object): + def __init__(self): + if not imgui.get_current_context(): + raise RuntimeError( + "No valid ImGui context. Use imgui.create_context() first and/or " + "imgui.set_current_context()." + ) + self.io = imgui.get_io() + + self._font_texture = None + + self.io.delta_time = 1.0 / 60.0 + + self._create_device_objects() + self.refresh_font_texture() + + def render(self, draw_data): + raise NotImplementedError + + def refresh_font_texture(self): + raise NotImplementedError + + def _create_device_objects(self): + raise NotImplementedError + + def _invalidate_device_objects(self): + raise NotImplementedError + + def shutdown(self): + self._invalidate_device_objects() + + +class ModernGLRenderer(BaseOpenGLRenderer): + + VERTEX_SHADER_SRC = """ + #version 330 + uniform mat4 ProjMtx; + in vec2 Position; + in vec2 UV; + in vec4 Color; + out vec2 Frag_UV; + out vec4 Frag_Color; + void main() { + Frag_UV = UV; + Frag_Color = Color; + gl_Position = ProjMtx * vec4(Position.xy, 0, 1); + } + """ + FRAGMENT_SHADER_SRC = """ + #version 330 + uniform sampler2D Texture; + in vec2 Frag_UV; + in vec4 Frag_Color; + out vec4 Out_Color; + void main() { + Out_Color = (Frag_Color * texture(Texture, Frag_UV.st)); + } + """ + + def __init__(self, *args, **kwargs): + self._prog = None + self._fbo = None + self._font_texture = None + self._vertex_buffer = None + self._index_buffer = None + self._vao = None + self._textures = {} + self.wnd = kwargs.get("wnd") + self.ctx = self.wnd.ctx if self.wnd and self.wnd.ctx else kwargs.get("ctx") + + if not self.ctx: + raise ValueError("Missing moderngl context") + + assert isinstance(self.ctx, moderngl.Context) + + super().__init__() + + if hasattr(self, "wnd") and self.wnd: + self.resize(*self.wnd.buffer_size) + elif "display_size" in kwargs: + self.io.display_size = kwargs.get("display_size") + + def register_texture(self, texture: moderngl.Texture): + """Make the imgui renderer aware of the texture""" + self._textures[texture.glo] = texture + + def remove_texture(self, texture: moderngl.Texture): + """Remove the texture from the imgui renderer""" + del self._textures[texture.glo] + + def refresh_font_texture(self): + font_matrix = self.io.fonts.get_tex_data_as_rgba32() + width = font_matrix.shape[1] + height = font_matrix.shape[0] + pixels = font_matrix.data + + if self._font_texture: + self.remove_texture(self._font_texture) + self._font_texture.release() + + self._font_texture = self.ctx.texture((width, height), 4, data=pixels) + self.register_texture(self._font_texture) + self.io.fonts.tex_id = self._font_texture.glo + self.io.fonts.clear_tex_data() + + def _create_device_objects(self): + self._prog = self.ctx.program( + vertex_shader=self.VERTEX_SHADER_SRC, + fragment_shader=self.FRAGMENT_SHADER_SRC, + ) + self.projMat = self._prog["ProjMtx"] + self._prog["Texture"].value = 0 + self._vertex_buffer = self.ctx.buffer(reserve=imgui.VERTEX_SIZE * 65536) + self._index_buffer = self.ctx.buffer(reserve=imgui.INDEX_SIZE * 65536) + self._vao = self.ctx.vertex_array( + self._prog, + [(self._vertex_buffer, "2f 2f 4f1", "Position", "UV", "Color")], + index_buffer=self._index_buffer, + index_element_size=imgui.INDEX_SIZE, + ) + + def render(self, draw_data: imgui.ImDrawData): + io = self.io + display_width, display_height = io.display_size + fb_width = int(display_width * io.display_framebuffer_scale[0]) + fb_height = int(display_height * io.display_framebuffer_scale[1]) + + if fb_width == 0 or fb_height == 0: + return + + self.projMat.value = ( + 2.0 / display_width, + 0.0, + 0.0, + 0.0, + 0.0, + 2.0 / -display_height, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + -1.0, + 1.0, + 0.0, + 1.0, + ) + + draw_data.scale_clip_rects(imgui.ImVec2(*io.display_framebuffer_scale)) + + self.ctx.enable_only(moderngl.BLEND) + self.ctx.blend_equation = moderngl.FUNC_ADD + self.ctx.blend_func = moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA + + self._font_texture.use() + + for commands in draw_data.cmd_lists: + # Write the vertex and index buffer data without copying it + vtx_type = ctypes.c_byte * commands.vtx_buffer.size() * imgui.VERTEX_SIZE + idx_type = ctypes.c_byte * commands.idx_buffer.size() * imgui.INDEX_SIZE + vtx_arr = (vtx_type).from_address(commands.vtx_buffer.data_address()) + idx_arr = (idx_type).from_address(commands.idx_buffer.data_address()) + self._vertex_buffer.write(vtx_arr) + self._index_buffer.write(idx_arr) + + idx_pos = 0 + for command in commands.cmd_buffer: + texture = self._textures.get(command.texture_id) + if texture is None: + raise ValueError( + ( + "Texture {} is not registered. Please add to renderer using " + "register_texture(..). " + "Current textures: {}".format( + command.texture_id, list(self._textures) + ) + ) + ) + + texture.use(0) + + x, y, z, w = command.clip_rect + self.ctx.scissor = int(x), int(fb_height - w), int(z - x), int(w - y) + self._vao.render( + moderngl.TRIANGLES, vertices=command.elem_count, first=idx_pos + ) + idx_pos += command.elem_count + + self.ctx.scissor = None + + def _invalidate_device_objects(self): + if self._font_texture: + self._font_texture.release() + if self._vertex_buffer: + self._vertex_buffer.release() + if self._index_buffer: + self._index_buffer.release() + if self._vao: + self._vao.release() + if self._prog: + self._prog.release() + + self.io.fonts.tex_id = 0 + self._font_texture = None + + +class ModernglWindowRenderer(ModernGLRenderer, ModernglWindowMixin): + def __init__(self, window): + super().__init__(wnd=window) + self.wnd = window + print('self.register_texture:', self.register_texture) + + self._init_key_maps() + self.io.display_size = self.wnd.size + # print(dir(self.io)) + self.io.display_framebuffer_scale = self.wnd.pixel_ratio, self.wnd.pixel_ratio + + def _init_key_maps(self): + keys = self.wnd.keys + + self.REVERSE_KEYMAP = { + keys.TAB: imgui.Key.tab, + keys.LEFT: imgui.Key.left_arrow, + keys.RIGHT: imgui.Key.right_arrow, + keys.UP: imgui.Key.up_arrow, + keys.DOWN: imgui.Key.down_arrow, + keys.PAGE_UP: imgui.Key.page_up, + keys.PAGE_DOWN: imgui.Key.page_down, + keys.HOME: imgui.Key.home, + keys.END: imgui.Key.end, + keys.DELETE: imgui.Key.delete, + keys.SPACE: imgui.Key.space, + keys.BACKSPACE: imgui.Key.backspace, + keys.ENTER: imgui.Key.enter, + keys.ESCAPE: imgui.Key.escape + } From 1c2d57397fc6c615b122cefe34c07e30e2fd3963 Mon Sep 17 00:00:00 2001 From: Jonte Date: Mon, 14 Oct 2024 10:44:57 +0200 Subject: [PATCH 2/3] Remove unwanted print statement --- moderngl_window/integrations/imgui_bundle.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/moderngl_window/integrations/imgui_bundle.py b/moderngl_window/integrations/imgui_bundle.py index 13f039f..b97bfd9 100644 --- a/moderngl_window/integrations/imgui_bundle.py +++ b/moderngl_window/integrations/imgui_bundle.py @@ -289,11 +289,9 @@ class ModernglWindowRenderer(ModernGLRenderer, ModernglWindowMixin): def __init__(self, window): super().__init__(wnd=window) self.wnd = window - print('self.register_texture:', self.register_texture) self._init_key_maps() self.io.display_size = self.wnd.size - # print(dir(self.io)) self.io.display_framebuffer_scale = self.wnd.pixel_ratio, self.wnd.pixel_ratio def _init_key_maps(self): From 9466f241a5cce9b74fad91f8ac2b71110f0a9f86 Mon Sep 17 00:00:00 2001 From: Jonte Date: Mon, 14 Oct 2024 10:47:48 +0200 Subject: [PATCH 3/3] Default to using imgui_bundle in the examples Replacing pyimgui with imgui_bundle in the examples. imgui_bundle is auto-generated, and has thus managed to follow the original project much more closely, whereas pyimgui is manually written and currently uses a 3.5 year old version of the Dear Imgui C++ implementation which is lacking a lot of new features. --- examples/integration_imgui.py | 9 +++++---- examples/integration_imgui_image.py | 11 ++++++----- examples/ssao.py | 9 ++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/examples/integration_imgui.py b/examples/integration_imgui.py index e0eb42f..c81107f 100644 --- a/examples/integration_imgui.py +++ b/examples/integration_imgui.py @@ -1,10 +1,11 @@ from pathlib import Path -import imgui +# import imgui +from imgui_bundle import imgui import moderngl from pyrr import Matrix44 import moderngl_window as mglw from moderngl_window import geometry -from moderngl_window.integrations.imgui import ModernglWindowRenderer +from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer class WindowEvents(mglw.WindowConfig): @@ -51,11 +52,11 @@ def render_ui(self): imgui.end_menu() imgui.end_main_menu_bar() - imgui.show_test_window() + imgui.show_demo_window() imgui.begin("Custom window", True) imgui.text("Bar") - imgui.text_colored("Eggs", 0.2, 1., 0.) + imgui.text_colored(imgui.ImVec4(0.2, 1., 0., 1.), "Eggs") imgui.end() imgui.render() diff --git a/examples/integration_imgui_image.py b/examples/integration_imgui_image.py index 2b8035b..b85590e 100644 --- a/examples/integration_imgui_image.py +++ b/examples/integration_imgui_image.py @@ -1,10 +1,11 @@ from pathlib import Path -import imgui +# import imgui +from imgui_bundle import imgui import moderngl from pyrr import Matrix44 import moderngl_window as mglw from moderngl_window import geometry -from moderngl_window.integrations.imgui import ModernglWindowRenderer +from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer import PIL @@ -68,11 +69,11 @@ def render_ui(self): imgui.end_menu() imgui.end_main_menu_bar() - imgui.show_test_window() + imgui.show_demo_window() imgui.begin("Custom window", True) imgui.text("Bar") - imgui.text_colored("Eggs", 0.2, 1., 0.) + imgui.text_colored(imgui.ImVec4(0.2, 1., 0., 1.), "Eggs") imgui.end() # Create window with the framebuffer image @@ -80,7 +81,7 @@ def render_ui(self): # Create an image control by passing in the OpenGL texture ID (glo) # and pass in the image size as well. # The texture needs to he registered using register_texture for this to work - imgui.image(self.fbo.color_attachments[0].glo, *self.fbo.size ) + imgui.image(self.fbo.color_attachments[0].glo, self.fbo.size ) imgui.end() imgui.render() diff --git a/examples/ssao.py b/examples/ssao.py index 48a6df0..1c09a88 100644 --- a/examples/ssao.py +++ b/examples/ssao.py @@ -1,12 +1,13 @@ -import imgui +# import imgui +from imgui_bundle import imgui import numpy as np from pathlib import Path import moderngl import moderngl_window from base import OrbitDragCameraWindow -from moderngl_window.integrations.imgui import ModernglWindowRenderer +from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer class SSAODemo(OrbitDragCameraWindow): @@ -185,9 +186,7 @@ def render_ui(self): _, self.base_color = imgui.color_edit3( "color", - self.base_color[0], - self.base_color[1], - self.base_color[2], + self.base_color ) _, self.material_properties[0] = imgui.slider_float("ambient", self.material_properties[0], 0.0, 1.0) _, self.material_properties[1] = imgui.slider_float("diffuse", self.material_properties[1], 0.0, 1.0)