Skip to content

Instantly share code, notes, and snippets.

@8Observer8
Created November 16, 2025 16:17
Show Gist options
  • Select an option

  • Save 8Observer8/994574cd77a5401aefa38990142aac9d to your computer and use it in GitHub Desktop.

Select an option

Save 8Observer8/994574cd77a5401aefa38990142aac9d to your computer and use it in GitHub Desktop.
Pygame and ModernGL. Keep aspect of rectangle and text
# 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()
@8Observer8
Copy link
Author

8Observer8 commented Nov 16, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment