Created
August 10, 2025 12:43
-
-
Save kadir014/3504eff51979fc0e6d6cc6146d4fcd42 to your computer and use it in GitHub Desktop.
Minimal imgui, moderngl and pygame with minimal dependencies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Minimal implementation of ImGUI using ModernGL and Pygame(-CE) requiring minimal dependencies. | |
# TODO: | |
- Better font handling? | |
- Key press events. | |
- Framebuffer scaling. | |
""" | |
from typing import Optional | |
import ctypes | |
import pygame | |
import moderngl | |
import imgui | |
WINDOW_WIDTH, WINDOW_HEIGHT = 640, 480 | |
class ImguiPygameModernGLAbomination: | |
""" | |
Processes pygame events, handles ModernGL rendering and ImGUI IO. | |
""" | |
def __init__(self, | |
display_size: tuple[int, int], | |
context: moderngl.Context, | |
vertex_shader: Optional[str] = None, | |
fragment_shader: Optional[str] = None | |
) -> None: | |
""" | |
Parameters | |
---------- | |
display_size | |
ImGUI display size. | |
context | |
ModernGL context created. | |
vertex_shader | |
Custom vertex shader. Uses the default one if not provided. | |
fragment_shader | |
Custom fragment shader. Uses the default one if not provided. | |
""" | |
self._context = context | |
imgui.create_context() | |
self.io = imgui.get_io() | |
self.io.display_size = display_size | |
self.io.delta_time = 1.0 / 60.0 | |
self._textures = {} | |
self.refresh_font_texture() | |
base_vertex_shader = """ | |
#version 330 | |
in vec2 in_position; | |
in vec2 in_uv; | |
in vec4 in_color; | |
out vec2 v_uv; | |
out vec4 v_color; | |
uniform mat4 u_proj; | |
void main() { | |
gl_Position = u_proj * vec4(in_position, 0.0, 1.0); | |
v_uv = in_uv; | |
v_color = in_color; | |
} | |
""" | |
base_fragment_shader = """ | |
#version 330 | |
in vec2 v_uv; | |
in vec4 v_color; | |
out vec4 f_color; | |
uniform sampler2D s_texture; | |
void main() { | |
f_color = v_color * texture(s_texture, v_uv); | |
} | |
""" | |
self._program = self._context.program( | |
vertex_shader=vertex_shader if vertex_shader else base_vertex_shader, | |
fragment_shader=fragment_shader if fragment_shader else base_fragment_shader, | |
) | |
# Taken from moderngl_window's integration, not sure why we multiply by 65536 | |
self._vbo = self._context.buffer(reserve=imgui.VERTEX_SIZE * 65536) | |
self._ibo = self._context.buffer(reserve=imgui.INDEX_SIZE * 65536) | |
self._proj = self._program["u_proj"] | |
self._vao = self._context.vertex_array( | |
self._program, | |
[ | |
(self._vbo, "2f 2f 4f1", "in_position", "in_uv", "in_color") | |
], | |
index_buffer=self._ibo, | |
index_element_size=imgui.INDEX_SIZE, | |
) | |
def __del__(self) -> None: | |
self.cleanup() | |
def cleanup(self) -> None: | |
""" Cleanup resources used. """ | |
for texture in self._textures: | |
self._textures[texture].release() | |
self._program.release() | |
self._vbo.release() | |
self._ibo.release() | |
self._vao.release() | |
def load_font(self, filepath: str, size: float) -> imgui.core._Font: | |
""" | |
Load custom TTF/OTF font into ImGUI. | |
Returns the loaded font object to be used when rendering. | |
Parameters | |
---------- | |
filepath | |
Path to the font file. | |
size | |
Glyph size in pixels. | |
""" | |
new_font = self.io.fonts.add_font_from_file_ttf(filepath, size) | |
self.refresh_font_texture() | |
return new_font | |
def refresh_font_texture(self) -> None: | |
""" Refresh the ImGUI font as a texture. """ | |
width, height, pixels = self.io.fonts.get_tex_data_as_rgba32() | |
font_texture = self._context.texture((width, height), 4, pixels) | |
self._textures[font_texture.glo] = font_texture | |
self.io.fonts.texture_id = font_texture.glo | |
self.io.fonts.clear_tex_data() | |
def resize(self, display_size: tuple[int, int]) -> None: | |
""" | |
Resize ImGUI display size. | |
Parameters | |
---------- | |
display_size | |
Tuple of new resolution. | |
""" | |
self.io.display_size = display_size | |
def process_events(self, events: list[pygame.Event]) -> None: | |
""" | |
Process pygame events and update ImGUI IO. | |
Parameters | |
---------- | |
events | |
List of pygame events (most likely from `pygame.event.get`) | |
""" | |
for event in events: | |
if event.type == pygame.MOUSEBUTTONDOWN: | |
self.io.mouse_down[event.button - 1] = True | |
if event.type == pygame.MOUSEBUTTONUP: | |
self.io.mouse_down[event.button - 1] = False | |
self.io.mouse_pos = pygame.mouse.get_pos() | |
def render(self, draw_data) -> None: | |
""" | |
Render ImGUI onto current framebuffer. | |
Note: This changes blending modes. | |
Parameters | |
---------- | |
draw_data | |
ImGUI draw commands (`imgui.get_draw_data`) | |
""" | |
display_width = self.io.display_size.x | |
display_height = self.io.display_size.y | |
# Thanks to moderngl_window, imgui's draw commands are very low level | |
self._context.enable_only(moderngl.BLEND) | |
self._context.blend_func = moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA | |
self._context.blend_equation = moderngl.FUNC_ADD | |
draw_data.scale_clip_rects(1.0, 1.0) | |
self._proj.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, | |
) | |
for commands in draw_data.commands_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) | |
idx_arr = (idx_type).from_address(commands.idx_buffer_data) | |
self._vbo.write(vtx_arr) | |
self._ibo.write(idx_arr) | |
idx_pos = 0 | |
for command in commands.commands: | |
# Texture's command id = moderngl `glo` attribute | |
texture = self._textures[command.texture_id] | |
texture.use(0) | |
x, y, z, w = command.clip_rect | |
self._context.scissor = int(x), int(display_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._context.scissor = None | |
if __name__ == "__main__": | |
pygame.init() | |
pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT), pygame.OPENGL | pygame.DOUBLEBUF) | |
clock = pygame.time.Clock() | |
context = moderngl.create_context() | |
helper = ImguiPygameModernGLAbomination((WINDOW_WIDTH, WINDOW_HEIGHT), context) | |
is_running = True | |
while is_running: | |
clock.tick(60) | |
pygame.display.set_caption(f"Pygame-CE ModernGL ImGUI @{round(clock.get_fps(), 1)}FPS") | |
events = pygame.event.get() | |
for event in events: | |
if event.type == pygame.QUIT: | |
is_running = False | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_ESCAPE: | |
is_running = False | |
helper.process_events(events) | |
context.clear(1.0, 1.0, 1.0) | |
imgui.new_frame() | |
imgui.begin("Nice window", True) | |
imgui.text("hell yeah") | |
imgui.end() | |
imgui.render() | |
helper.render(imgui.get_draw_data()) | |
pygame.display.flip() | |
helper.cleanup() | |
context.release() | |
pygame.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment