Created
November 16, 2025 16:17
-
-
Save 8Observer8/994574cd77a5401aefa38990142aac9d to your computer and use it in GitHub Desktop.
Pygame and ModernGL. Keep aspect of rectangle and text
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
| # pip install moderngl pygame numpy pyglm | |
| from typing import Optional, Tuple | |
| import glm | |
| import moderngl | |
| import numpy as np | |
| import pygame as pg | |
| # ---------------- TYPES ---------------- # | |
| Color = Tuple[int, int, int, int] # (R, G, B, A) | |
| class WindowSettings: | |
| def __init__(self, size=(640, 480), clear_color=(0, 0, 0, 255)): | |
| self.size = size | |
| self.clear_color = clear_color | |
| class IRenderer: | |
| def __init__(self, settings: WindowSettings): | |
| self.settings = settings | |
| # ---------------- SHADERS ---------------- # | |
| RECT_VERTEX_SHADER = """ | |
| #version 330 core | |
| in vec2 in_vert; | |
| in vec4 in_color; | |
| uniform mat4 uMvpMatrix; | |
| out vec4 v_color; | |
| void main() { | |
| gl_Position = uMvpMatrix * vec4(in_vert, 0.0, 1.0); | |
| v_color = in_color; | |
| } | |
| """ | |
| RECT_FRAGMENT_SHADER = """ | |
| #version 330 core | |
| in vec4 v_color; | |
| out vec4 f_color; | |
| void main() { | |
| f_color = v_color; | |
| } | |
| """ | |
| TEXT_VERTEX_SHADER = """ | |
| #version 330 core | |
| in vec2 in_vert; | |
| in vec2 in_tex; | |
| uniform mat4 uMvpMatrix; | |
| out vec2 v_tex; | |
| void main() { | |
| gl_Position = uMvpMatrix * vec4(in_vert, 0.0, 1.0); | |
| v_tex = in_tex; | |
| } | |
| """ | |
| TEXT_FRAGMENT_SHADER = """ | |
| #version 330 core | |
| in vec2 v_tex; | |
| out vec4 f_color; | |
| uniform sampler2D texture0; | |
| uniform vec4 color; | |
| void main() { | |
| vec4 tex_color = texture(texture0, v_tex); | |
| f_color = vec4(color.rgb, tex_color.a * color.a); | |
| } | |
| """ | |
| # ---------------- RENDERER ---------------- # | |
| class ModernGLRenderer(IRenderer): | |
| """2D renderer using ModernGL in pixel coordinates""" | |
| def __init__(self, settings: WindowSettings): | |
| super().__init__(settings) | |
| # Create window | |
| flags = pg.OPENGL | pg.DOUBLEBUF | pg.RESIZABLE | |
| self._screen = pg.display.set_mode(settings.size, flags) | |
| self.ctx = moderngl.create_context() | |
| self.ctx.enable(moderngl.BLEND) | |
| self.ctx.blend_func = (moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA) | |
| # Programs | |
| self.rect_program = self.ctx.program( | |
| vertex_shader=RECT_VERTEX_SHADER, | |
| fragment_shader=RECT_FRAGMENT_SHADER | |
| ) | |
| self.text_program = self.ctx.program( | |
| vertex_shader=TEXT_VERTEX_SHADER, | |
| fragment_shader=TEXT_FRAGMENT_SHADER | |
| ) | |
| # Projection matrix | |
| self._update_projection() | |
| # Font | |
| pg.font.init() | |
| self.font = pg.font.SysFont('Arial', 24) | |
| # ---------------- PROJECTION ---------------- # | |
| def _update_projection(self): | |
| width, height = self.settings.size | |
| # Orthographic projection in pixels (0,0 top-left, width,height bottom-right) | |
| self.proj_matrix = glm.ortho(0.0, width, height, 0.0, -1.0, 1.0) | |
| def handle_resize(self, new_width: int, new_height: int): | |
| self.settings.size = (new_width, new_height) | |
| self.ctx.viewport = (0, 0, new_width, new_height) | |
| self._update_projection() | |
| # ---------------- COLOR UTILS ---------------- # | |
| @staticmethod | |
| def _color_normalize(color: Color): | |
| return tuple(c / 255.0 for c in color) | |
| # ---------------- FRAME ---------------- # | |
| def begin_frame(self): | |
| self.clear() | |
| def end_frame(self): | |
| pg.display.flip() | |
| def clear(self, color: Optional[Color] = None): | |
| nor_color = self._color_normalize(color if color else self.settings.clear_color) | |
| self.ctx.clear(*nor_color) | |
| # ---------------- DRAW RECTANGLE ---------------- # | |
| def draw_rectangle(self, x: float, y: float, width: float, height: float, color: Color = (255, 255, 255, 255)): | |
| color_normalized = self._color_normalize(color) | |
| vertices = np.array([ | |
| 0.0, 0.0, *color_normalized, | |
| width, 0.0, *color_normalized, | |
| 0.0, height, *color_normalized, | |
| width, 0.0, *color_normalized, | |
| width, height, *color_normalized, | |
| 0.0, height, *color_normalized, | |
| ], dtype='f4') | |
| vbo = self.ctx.buffer(vertices.tobytes()) | |
| vao = self.ctx.vertex_array(self.rect_program, [(vbo, '2f 4f', 'in_vert', 'in_color')]) | |
| # Model matrix (position only) | |
| model = glm.mat4(1.0) | |
| model = glm.translate(model, glm.vec3(x, y, 0.0)) | |
| mvp = self.proj_matrix * model | |
| # Correct conversion to bytes | |
| mvp_bytes = np.array(mvp.to_list(), dtype='f4').tobytes() | |
| self.rect_program['uMvpMatrix'].write(mvp_bytes) | |
| vao.render() | |
| vbo.release() | |
| vao.release() | |
| # ---------------- DRAW TEXT ---------------- # | |
| def draw_text(self, text: str, x: float, y: float, color: Color = (255, 255, 255, 255)): | |
| color_normalized = self._color_normalize(color) | |
| text_surface = self.font.render(text, True, (255, 255, 255)) | |
| w, h = text_surface.get_size() | |
| texture_data = pg.image.tostring(text_surface, 'RGBA') | |
| texture = self.ctx.texture((w, h), 4, texture_data) | |
| texture.filter = (moderngl.LINEAR, moderngl.LINEAR) | |
| vertices = np.array([ | |
| 0.0, 0.0, 0.0, 0.0, | |
| w, 0.0, 1.0, 0.0, | |
| 0.0, h, 0.0, 1.0, | |
| w, 0.0, 1.0, 0.0, | |
| w, h, 1.0, 1.0, | |
| 0.0, h, 0.0, 1.0, | |
| ], dtype='f4') | |
| vbo = self.ctx.buffer(vertices.tobytes()) | |
| vao = self.ctx.vertex_array(self.text_program, [(vbo, '2f 2f', 'in_vert', 'in_tex')]) | |
| # Model matrix | |
| model = glm.mat4(1.0) | |
| model = glm.translate(model, glm.vec3(x, y, 0.0)) | |
| mvp = self.proj_matrix * model | |
| mvp_bytes = np.array(mvp.to_list(), dtype='f4').tobytes() | |
| self.text_program['uMvpMatrix'].write(mvp_bytes) | |
| self.text_program['color'].value = color_normalized | |
| texture.use(0) | |
| vao.render() | |
| texture.release() | |
| vbo.release() | |
| vao.release() | |
| # ---------------- MAIN ---------------- # | |
| def main(): | |
| pg.init() | |
| settings = WindowSettings(size=(600, 400), clear_color=(50, 50, 50, 255)) | |
| renderer = ModernGLRenderer(settings) | |
| running = True | |
| while running: | |
| for event in pg.event.get(): | |
| if event.type == pg.QUIT: | |
| running = False | |
| elif event.type == pg.VIDEORESIZE: | |
| renderer.handle_resize(*event.size) | |
| renderer.begin_frame() | |
| renderer.draw_rectangle(50, 50, 200, 100, (255, 0, 0, 255)) | |
| renderer.draw_text("Hello ModernGL!", 300, 200, (0, 255, 0, 255)) | |
| renderer.end_frame() | |
| pg.quit() | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
CyberForum topic