Created
June 22, 2025 13:02
-
-
Save EncodeTheCode/b6424efface62532b930de75419d7789 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 * | |
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