Created
June 22, 2025 12:43
-
-
Save EncodeTheCode/44b159f8f5586792b17a276808925cee to your computer and use it in GitHub Desktop.
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
import pygame | |
import numpy as np | |
from pygame.locals import QUIT, KEYDOWN, KEYUP | |
from OpenGL.GL import * | |
from OpenGL.GLU import * | |
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 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,2.0,5.0] | |
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