Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created June 22, 2025 13:02
Show Gist options
  • Save EncodeTheCode/b6424efface62532b930de75419d7789 to your computer and use it in GitHub Desktop.
Save EncodeTheCode/b6424efface62532b930de75419d7789 to your computer and use it in GitHub Desktop.
import pygame
import numpy as np
from pygame.locals import QUIT, KEYDOWN, KEYUP
from OpenGL.GL import *
from OpenGL.GLU import *
import sys
class Engine:
def __init__(self, w, h,
fov=np.pi/4, near=0.1, far=100.0,
init_pos=None, init_yaw=-90.0, init_pitch=0.0):
# Initialize Pygame + OpenGL context
pygame.init()
pygame.display.set_mode((w, h), pygame.DOUBLEBUF | pygame.OPENGL)
pygame.display.set_caption("3D Renderer - Optimized")
self.clock = pygame.time.Clock()
self.w, self.h = w, h
self.aspect = w / h
# Camera parameters
self.pos = np.array(init_pos if init_pos is not None else [-1.0, 5.0, 13.0], dtype=np.float32)
self.yaw, self.pitch = init_yaw, init_pitch
self._update_dir()
self.speed = 0.1
self.sensitivity = 0.1
self.keys = {K: False for K in [pygame.K_w, pygame.K_s,
pygame.K_a, pygame.K_d,
pygame.K_SPACE, pygame.K_LSHIFT]}
self.left_down = False
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
# Projection matrix (once)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(np.degrees(fov), self.aspect, near, far)
glMatrixMode(GL_MODELVIEW)
# Enable depth and textures
glEnable(GL_DEPTH_TEST)
glEnable(GL_TEXTURE_2D)
# Placeholder for display list and textures
self.model_list = None
self.texture_map = {}
def _update_dir(self):
ry, rp = np.radians(self.yaw), np.radians(self.pitch)
dir_vec = np.array([
np.cos(ry) * np.cos(rp),
np.sin(rp),
np.sin(ry) * np.cos(rp)
], dtype=np.float32)
self.dir = dir_vec / np.linalg.norm(dir_vec)
def handle_input(self):
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit(); quit()
elif event.type == KEYUP and event.key == pygame.K_ESCAPE:
pygame.quit(); sys.exit()
elif event.type in (KEYDOWN, KEYUP) and event.key in self.keys:
self.keys[event.key] = (event.type == KEYDOWN)
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
self.left_down = True
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
self.left_down = False
elif event.type == pygame.MOUSEMOTION and self.left_down:
dx, dy = event.rel
self.yaw += dx * self.sensitivity
self.pitch = np.clip(self.pitch - dy * self.sensitivity, -89.0, 89.0)
self._update_dir()
def update_camera(self):
forward = self.dir * self.speed
right = np.cross(self.dir, [0,1,0])
right = right / np.linalg.norm(right) * self.speed
upv = np.array([0,1,0], dtype=np.float32) * self.speed
if self.keys[pygame.K_w]: self.pos += forward
if self.keys[pygame.K_s]: self.pos -= forward
if self.keys[pygame.K_a]: self.pos -= right
if self.keys[pygame.K_d]: self.pos += right
if self.keys[pygame.K_SPACE]: self.pos += upv
if self.keys[pygame.K_LSHIFT]: self.pos -= upv
def set_camera(self):
center = self.pos + self.dir
glLoadIdentity()
gluLookAt(*self.pos, *center, 0,1,0)
def load_obj(self, obj_file, mtl_file=None):
# Parse OBJ
verts, texs, faces, face_mats = [], [], [], []
current_mat = None
materials = {}
with open(obj_file) as f:
for line in f:
parts = line.split()
if not parts: continue
if parts[0]=='v': verts.append(list(map(float, parts[1:])))
elif parts[0]=='vt': texs.append(list(map(float, parts[1:3])))
elif parts[0]=='f':
idx = []
for v in parts[1:]:
vi, ti = v.split('/')[:2]
idx.append((int(vi)-1, int(ti)-1 if ti else None))
faces.append(idx)
face_mats.append(current_mat)
elif parts[0]=='usemtl': current_mat = parts[1]
# Parse MTL
if mtl_file:
mats = {}
with open(mtl_file) as f:
for line in f:
p = line.split()
if not p: continue
if p[0]=='newmtl': mat = p[1]; mats[mat]={}
elif p[0]=='map_Kd': mats[mat]['diffuse'] = p[1]
materials = mats
# Load textures
for mat, data in materials.items():
if 'diffuse' in data:
tex_id = self.load_texture(data['diffuse'])
self.texture_map[mat] = tex_id
# Build display list
self.model_list = glGenLists(1)
glNewList(self.model_list, GL_COMPILE)
glEnable(GL_TEXTURE_2D)
for face, mat in zip(faces, face_mats):
if mat and mat in self.texture_map:
glBindTexture(GL_TEXTURE_2D, self.texture_map[mat])
glBegin(GL_POLYGON)
for vi, ti in face:
if ti is not None and ti < len(texs):
u,v = texs[ti]
glTexCoord2f(u,v)
x,y,z = verts[vi]
glVertex3f(x,y,z)
glEnd()
glDisable(GL_TEXTURE_2D)
glEndList()
def load_texture(self, img_file):
surf = pygame.image.load(img_file)
data = pygame.image.tostring(surf, 'RGBA', True)
w,h = surf.get_size()
tex = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, tex)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
return tex
def render(self):
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
self.set_camera()
glCallList(self.model_list)
def run(self, obj_file, mtl_file=None):
self.load_obj(obj_file, mtl_file)
while True:
self.handle_input()
self.update_camera()
self.render()
pygame.display.flip()
self.clock.tick(60)
if __name__ == '__main__':
init_pos = [0.0,15.0,7.5]
e = Engine(800,600, init_pos=init_pos)
e.run('Barbara.obj', 'Barbara.mtl')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment