Created
November 16, 2025 16:20
-
-
Save 8Observer8/a8f41f104bb33fe99b54b39c8687cf31 to your computer and use it in GitHub Desktop.
Pygame and ModernGL. Move a camera with WASD and arrow keys
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) | |
| # Camera (NEW) | |
| self.cam_pos = glm.vec3(0, 0, 5) | |
| self.cam_target = glm.vec3(0, 0, 0) | |
| self.cam_up = glm.vec3(0, 1, 0) | |
| self.view_matrix = glm.lookAt(self.cam_pos, self.cam_target, self.cam_up) | |
| # 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 | |
| self.proj_matrix = glm.ortho(0.0, width, height, 0.0, -10.0, 10.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() | |
| # ---------------- CAMERA UPDATE (NEW) ---------------- # | |
| def update_camera(self, dt): | |
| speed = 300.0 * dt # pixels per second | |
| keys = pg.key.get_pressed() | |
| # WASD movement | |
| if keys[pg.K_w]: | |
| self.cam_pos.y -= speed | |
| if keys[pg.K_s]: | |
| self.cam_pos.y += speed | |
| if keys[pg.K_a]: | |
| self.cam_pos.x -= speed | |
| if keys[pg.K_d]: | |
| self.cam_pos.x += speed | |
| # Arrow keys (same as WASD) | |
| if keys[pg.K_UP]: | |
| self.cam_pos.y -= speed | |
| if keys[pg.K_DOWN]: | |
| self.cam_pos.y += speed | |
| if keys[pg.K_LEFT]: | |
| self.cam_pos.x -= speed | |
| if keys[pg.K_RIGHT]: | |
| self.cam_pos.x += speed | |
| # Recreate view matrix | |
| self.cam_target = glm.vec3(self.cam_pos.x, self.cam_pos.y, 0) | |
| self.view_matrix = glm.lookAt(self.cam_pos, self.cam_target, self.cam_up) | |
| # ---------------- 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) | |
| # ---------------- BUILD MVP ---------------- # | |
| def _make_mvp(self, model): | |
| mvp = self.proj_matrix * self.view_matrix * model | |
| return np.array(mvp.to_list(), dtype="f4").tobytes() | |
| # ---------------- DRAW RECTANGLE ---------------- # | |
| def draw_rectangle(self, x, y, width, height, 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 = glm.translate(glm.mat4(1.0), glm.vec3(x, y, 0)) | |
| self.rect_program['uMvpMatrix'].write(self._make_mvp(model)) | |
| 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 = glm.translate(glm.mat4(1.0), glm.vec3(x, y, 0)) | |
| self.text_program['uMvpMatrix'].write(self._make_mvp(model)) | |
| self.text_program['color'].value = color_normalized | |
| texture.use(0) | |
| vao.render() | |
| texture.release() | |
| vbo.release() | |
| vao.release() | |
| # ---------------- MAIN ---------------- # | |
| def main(): | |
| pg.init() | |
| clock = pg.time.Clock() | |
| settings = WindowSettings(size=(600, 400), clear_color=(50, 50, 50, 255)) | |
| renderer = ModernGLRenderer(settings) | |
| running = True | |
| while running: | |
| dt = clock.tick(60) / 1000.0 # seconds | |
| for event in pg.event.get(): | |
| if event.type == pg.QUIT: | |
| running = False | |
| elif event.type == pg.VIDEORESIZE: | |
| renderer.handle_resize(*event.size) | |
| # Update camera each frame | |
| renderer.update_camera(dt) | |
| renderer.begin_frame() | |
| renderer.draw_rectangle(50, 50, 200, 100, (255, 0, 0, 255)) | |
| renderer.draw_text("Camera WASD + Arrows!", 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