Engine Code

This commit is contained in:
Exil Productions
2025-04-07 22:07:52 +02:00
committed by GitHub
parent b940113bf2
commit fb3b098299
16 changed files with 1147 additions and 0 deletions

38
assets/cube.obj Normal file
View File

@@ -0,0 +1,38 @@
# Blender 4.4.0
# www.blender.org
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vn -0.0000 1.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -1.0000 -0.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vt 0.625000 0.500000
vt 0.875000 0.500000
vt 0.875000 0.750000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.125000 0.500000
vt 0.375000 0.500000
vt 0.125000 0.750000
s 0
f 1/1/1 5/2/1 7/3/1 3/4/1
f 4/5/2 3/4/2 7/6/2 8/7/2
f 8/8/3 7/9/3 5/10/3 6/11/3
f 6/12/4 2/13/4 4/5/4 8/14/4
f 2/13/5 1/1/5 3/4/5 4/5/5
f 6/11/6 5/10/6 1/1/6 2/13/6

0
engine/__init__.py Normal file
View File

85
engine/camera.py Normal file
View File

@@ -0,0 +1,85 @@
import numpy as np
import pyrr
class Camera:
def __init__(self, position=np.array([0.0, 0.0, 3.0], dtype=np.float32),
target=np.array([0.0, 0.0, 0.0], dtype=np.float32),
up=np.array([0.0, 1.0, 0.0], dtype=np.float32),
yaw=-90.0, pitch=0.0):
self.position = np.array(position, dtype=np.float32)
self.world_up = np.array(up, dtype=np.float32)
self.yaw = yaw
self.pitch = pitch
self.zoom = 45.0
if target is not None:
target = np.array(target, dtype=np.float32)
direction = target - self.position
direction = direction / np.linalg.norm(direction)
self.yaw = np.degrees(np.arctan2(direction[2], direction[0]))
self.pitch = np.degrees(np.arcsin(direction[1]))
self._update_camera_vectors()
def _update_camera_vectors(self):
front = np.array([
np.cos(np.radians(self.yaw)) * np.cos(np.radians(self.pitch)),
np.sin(np.radians(self.pitch)),
np.sin(np.radians(self.yaw)) * np.cos(np.radians(self.pitch))
], dtype=np.float32)
self.front = front / np.linalg.norm(front)
self.right = np.cross(self.front, self.world_up)
self.right = self.right / np.linalg.norm(self.right)
self.up = np.cross(self.right, self.front)
self.up = self.up / np.linalg.norm(self.up)
def get_view_matrix(self):
return pyrr.matrix44.create_look_at(self.position, self.position + self.front, self.up)
def get_projection_matrix(self, aspect_ratio):
return pyrr.matrix44.create_perspective_projection(
self.zoom, aspect_ratio, 0.1, 100.0
)
def set_position(self, position):
self.position = np.array(position, dtype=np.float32)
self._update_camera_vectors()
# moving camera, is extremely yanky but does the job
def move(self, direction, amount):
if direction == "forward":
self.position += self.front * amount
elif direction == "backward":
self.position -= self.front * amount
elif direction == "left":
self.position -= self.right * amount
elif direction == "right":
self.position += self.right * amount
elif direction == "up":
self.position += self.up * amount
elif direction == "down":
self.position -= self.up * amount
def rotate(self, yaw_offset, pitch_offset, constrain_pitch=True):
self.yaw += yaw_offset
self.pitch += pitch_offset
if constrain_pitch:
if self.pitch > 89.0:
self.pitch = 89.0
if self.pitch < -89.0:
self.pitch = -89.0
self._update_camera_vectors()
# basically the fov
def set_zoom(self, zoom):
self.zoom = zoom
if self.zoom < 1.0:
self.zoom = 1.0
if self.zoom > 45.0:
self.zoom = 45.0

75
engine/engine.py Normal file
View File

@@ -0,0 +1,75 @@
import os
from .window import Window
from .scene import Scene
from .shader import Shader
class Engine:
def __init__(self, width=800, height=600, title="3D Game Render Engine"):
self.window = Window(width, height, title)
self.scene = Scene()
self.running = False
self.last_time = 0
self.delta_time = 0
self.default_shader = None
self._init_default_shader()
#get shaders from shader dir
def _init_default_shader(self):
shaders_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "shaders")
vert_shader = os.path.join(shaders_dir, "phong.vert")
frag_shader = os.path.join(shaders_dir, "phong.frag")
self.default_shader = Shader(vert_shader, frag_shader)
def start(self):
import glfw
self.running = True
self.last_time = glfw.get_time()
while self.running and not self.window.should_close():
current_time = glfw.get_time()
self.delta_time = current_time - self.last_time
self.last_time = current_time
self.window.poll_events()
self.update(self.delta_time)
self.render()
self.window.swap_buffers()
self.shutdown()
def stop(self):
self.running = False
#only gets used by application
def update(self, delta_time):
pass
def render(self):
self.window.clear(0.1, 0.1, 0.1, 1.0)
camera = self.scene.camera
if not camera:
return
aspect_ratio = self.window.get_aspect_ratio()
view_matrix = camera.get_view_matrix()
projection_matrix = camera.get_projection_matrix(aspect_ratio)
self.default_shader.use()
self.default_shader.set_mat4("view", view_matrix)
self.default_shader.set_mat4("projection", projection_matrix)
self.default_shader.set_vec3("viewPos", camera.position)
self.scene.render(self.default_shader)
def shutdown(self):
self.window.terminate()
def get_delta_time(self):
return self.delta_time
def get_window(self):
"""Get the window"""
return self.window
def get_scene(self):
"""Get the scene"""
return self.scene

65
engine/light.py Normal file
View File

@@ -0,0 +1,65 @@
import numpy as np
class Light:
def __init__(self, ambient, diffuse, specular):
self.ambient = ambient
self.diffuse = diffuse
self.specular = specular
def apply(self, shader, index):
pass
#here comes the sun duh duh duh duh
class DirectionalLight(Light):
def __init__(self, direction, ambient, diffuse, specular):
super().__init__(ambient, diffuse, specular)
self.direction = direction
def apply(self, shader, index):
shader.set_vec3(f"dirLight.direction", self.direction)
shader.set_vec3(f"dirLight.ambient", self.ambient)
shader.set_vec3(f"dirLight.diffuse", self.diffuse)
shader.set_vec3(f"dirLight.specular", self.specular)
#point light implementation
class PointLight(Light):
def __init__(self, position, ambient, diffuse, specular, constant, linear, quadratic):
super().__init__(ambient, diffuse, specular)
self.position = position
self.constant = constant
self.linear = linear
self.quadratic = quadratic
def apply(self, shader, index):
shader.set_vec3(f"pointLights[{index}].position", self.position)
shader.set_vec3(f"pointLights[{index}].ambient", self.ambient)
shader.set_vec3(f"pointLights[{index}].diffuse", self.diffuse)
shader.set_vec3(f"pointLights[{index}].specular", self.specular)
shader.set_float(f"pointLights[{index}].constant", self.constant)
shader.set_float(f"pointLights[{index}].linear", self.linear)
shader.set_float(f"pointLights[{index}].quadratic", self.quadratic)
class SpotLight(Light):
def __init__(self, position, direction, ambient, diffuse, specular,
constant, linear, quadratic, cut_off, outer_cut_off):
super().__init__(ambient, diffuse, specular)
self.position = position
self.direction = direction
self.constant = constant
self.linear = linear
self.quadratic = quadratic
self.cut_off = cut_off
self.outer_cut_off = outer_cut_off
def apply(self, shader, index):
shader.set_vec3(f"spotLight.position", self.position)
shader.set_vec3(f"spotLight.direction", self.direction)
shader.set_vec3(f"spotLight.ambient", self.ambient)
shader.set_vec3(f"spotLight.diffuse", self.diffuse)
shader.set_vec3(f"spotLight.specular", self.specular)
shader.set_float(f"spotLight.constant", self.constant)
shader.set_float(f"spotLight.linear", self.linear)
shader.set_float(f"spotLight.quadratic", self.quadratic)
shader.set_float(f"spotLight.cutOff", self.cut_off)
shader.set_float(f"spotLight.outerCutOff", self.outer_cut_off)

128
engine/mesh.py Normal file
View File

@@ -0,0 +1,128 @@
from OpenGL.GL import *
import numpy as np
import open3d as o3d
import ctypes
class Mesh:
def __init__(self, o3d_mesh=None, vertices=None, indices=None, textures=None):
self.textures = textures if textures else []
self.VAO = glGenVertexArrays(1)
self.VBO = glGenBuffers(1)
self.EBO = glGenBuffers(1)
#if we use a mesh from open3d we should use vertices and indices
if o3d_mesh is not None:
self._init_from_open3d(o3d_mesh)
#if not just use the vertices and indices
elif vertices is not None and indices is not None:
self._init_from_arrays(vertices, indices)
else:
raise ValueError("Either an Open3D mesh or vertices and indices must be provided")
def _init_from_open3d(self, o3d_mesh):
if not o3d_mesh.has_vertex_normals():
o3d_mesh.compute_vertex_normals()
vertices = np.asarray(o3d_mesh.vertices)
normals = np.asarray(o3d_mesh.vertex_normals)
if o3d_mesh.has_triangle_uvs():
uvs = np.asarray(o3d_mesh.triangle_uvs)
#this is only some workaround though, still need to implement this right
if len(uvs) > 0:
tex_coords = np.zeros((len(vertices), 2), dtype=np.float32)
for i, triangle in enumerate(o3d_mesh.triangles):
for j in range(3):
tex_coords[triangle[j]] = uvs[i * 3 + j]
else:
tex_coords = np.zeros((len(vertices), 2), dtype=np.float32)
else:
tex_coords = np.zeros((len(vertices), 2), dtype=np.float32)
vertex_data = np.zeros((len(vertices), 8), dtype=np.float32)
vertex_data[:, 0:3] = vertices
vertex_data[:, 3:6] = normals
vertex_data[:, 6:8] = tex_coords
indices = np.asarray(o3d_mesh.triangles).flatten()
self._init_from_arrays(vertex_data, indices)
self.o3d_mesh = o3d_mesh
def _init_from_arrays(self, vertices, indices):
self.vertices = vertices
self.indices = indices
glBindVertexArray(self.VAO)
glBindBuffer(GL_ARRAY_BUFFER, self.VBO)
glBufferData(GL_ARRAY_BUFFER, self.vertices.nbytes, self.vertices, GL_STATIC_DRAW)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.EBO)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, self.indices.nbytes, self.indices, GL_STATIC_DRAW)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(3 * 4))
glEnableVertexAttribArray(1)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(6 * 4))
glEnableVertexAttribArray(2)
glBindVertexArray(0)
@staticmethod
def create_box(width=1.0, height=1.0, depth=1.0):
box = o3d.geometry.TriangleMesh.create_box(width=width, height=height, depth=depth)
box.compute_vertex_normals()
return Mesh(o3d_mesh=box)
@staticmethod
def create_sphere(radius=1.0, resolution=20):
sphere = o3d.geometry.TriangleMesh.create_sphere(radius=radius, resolution=resolution)
sphere.compute_vertex_normals()
return Mesh(o3d_mesh=sphere)
@staticmethod
def create_cylinder(radius=1.0, height=2.0, resolution=20, split=4):
cylinder = o3d.geometry.TriangleMesh.create_cylinder(radius=radius, height=height, resolution=resolution,
split=split)
cylinder.compute_vertex_normals()
return Mesh(o3d_mesh=cylinder)
@staticmethod
def create_cone(radius=1.0, height=2.0, resolution=20, split=1):
cone = o3d.geometry.TriangleMesh.create_cone(radius=radius, height=height, resolution=resolution, split=split)
cone.compute_vertex_normals()
return Mesh(o3d_mesh=cone)
@staticmethod
def create_torus(torus_radius=1.0, tube_radius=0.2, radial_resolution=30, tubular_resolution=20):
torus = o3d.geometry.TriangleMesh.create_torus(torus_radius=torus_radius, tube_radius=tube_radius,
radial_resolution=radial_resolution,
tubular_resolution=tubular_resolution)
torus.compute_vertex_normals()
return Mesh(o3d_mesh=torus)
def draw(self, shader):
diffuse_nr = 1
specular_nr = 1
for i, texture in enumerate(self.textures):
glActiveTexture(GL_TEXTURE0 + i)
number = ""
name = texture.type
if name == "texture_diffuse":
number = str(diffuse_nr)
diffuse_nr += 1
elif name == "texture_specular":
number = str(specular_nr)
specular_nr += 1
shader.set_int(f"material.{name}{number}", i)
glBindTexture(GL_TEXTURE_2D, texture.id)
glBindVertexArray(self.VAO)
glDrawElements(GL_TRIANGLES, len(self.indices), GL_UNSIGNED_INT, None)
glBindVertexArray(0)
glActiveTexture(GL_TEXTURE0)

91
engine/mesh_utils.py Normal file
View File

@@ -0,0 +1,91 @@
import open3d as o3d
import numpy as np
from .mesh import Mesh
def mesh_from_point(points, normals=None, colors=None):
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
if normals is not None:
pcd.normals = o3d.utility.Vector3dVector(normals)
else:
pcd.estimate_normals()
if colors is not None:
pcd.colors = o3d.utility.Vector3dVector(colors)
mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd, depth=8)
return Mesh(o3d_mesh=mesh)
def merge_meshes(meshes):
if not meshes:
return None
o3d_meshes = []
for mesh in meshes:
if hasattr(mesh, 'o3d_mesh'):
o3d_meshes.append(mesh.o3d_mesh)
else:
o3d_meshes.append(mesh)
merged_mesh = o3d_meshes[0]
for mesh in o3d_meshes[1:]:
merged_mesh += mesh
if not merged_mesh.has_vertex_normals():
merged_mesh.compute_vertex_normals()
return Mesh(o3d_mesh=merged_mesh)
def subdivide_mesh(mesh, iterations=1):
if hasattr(mesh, 'o3d_mesh'):
o3d_mesh = mesh.o3d_mesh
else:
o3d_mesh = mesh
subdivided_mesh = o3d_mesh.subdivide_midpoint(number_of_iterations=iterations)
if not subdivided_mesh.has_vertex_normals():
subdivided_mesh.compute_vertex_normals()
return Mesh(o3d_mesh=subdivided_mesh)
#basically decimate
def simplify_mesh(mesh, target_triangles):
if hasattr(mesh, 'o3d_mesh'):
o3d_mesh = mesh.o3d_mesh
else:
o3d_mesh = mesh
simplified_mesh = o3d_mesh.simplify_quadric_decimation(target_number_of_triangles=target_triangles)
if not simplified_mesh.has_vertex_normals():
simplified_mesh.compute_vertex_normals()
return Mesh(o3d_mesh=simplified_mesh)
def create_terrain(width, length, height_function, resolution=100):
x = np.linspace(-width / 2, width / 2, resolution)
z = np.linspace(-length / 2, length / 2, resolution)
X, Z = np.meshgrid(x, z)
Y = np.zeros_like(X)
for i in range(resolution):
for j in range(resolution):
Y[i, j] = height_function(X[i, j], Z[i, j])
vertices = []
for i in range(resolution):
for j in range(resolution):
vertices.append([X[i, j], Y[i, j], Z[i, j]])
triangles = []
for i in range(resolution - 1):
for j in range(resolution - 1):
idx1 = i * resolution + j
idx2 = i * resolution + (j + 1)
idx3 = (i + 1) * resolution + j
idx4 = (i + 1) * resolution + (j + 1)
triangles.append([idx1, idx2, idx3])
triangles.append([idx2, idx4, idx3])
o3d_mesh = o3d.geometry.TriangleMesh()
o3d_mesh.vertices = o3d.utility.Vector3dVector(vertices)
o3d_mesh.triangles = o3d.utility.Vector3iVector(triangles)
o3d_mesh.compute_vertex_normals()
return Mesh(o3d_mesh=o3d_mesh)

117
engine/model.py Normal file
View File

@@ -0,0 +1,117 @@
import numpy as np
import pyrr
import open3d as o3d
from .mesh import Mesh
import os
from .obj_loader import load_obj
class Model:
def __init__(self, path=None, mesh=None):
self.meshes = []
self.textures_loaded = {}
self.position = np.array([0.0, 0.0, 0.0])
self.rotation = np.array([0.0, 0.0, 0.0])
self.scale = np.array([1.0, 1.0, 1.0])
self.color = np.array([0.8, 0.8, 0.8])
self.shininess = 32.0
if path:
self._load_model(path)
elif mesh:
self.meshes.append(mesh)
else:
self._create_cube()
def _load_model(self, path):
directory = os.path.dirname(path)
if path.lower().endswith('.obj'):
mesh = load_obj(path, directory)
self.meshes.append(mesh)
elif path.lower().endswith('.ply'):
o3d_mesh = o3d.io.read_triangle_mesh(path)
if not o3d_mesh.has_vertex_normals():
o3d_mesh.compute_vertex_normals()
mesh = Mesh(o3d_mesh=o3d_mesh)
self.meshes.append(mesh)
elif path.lower().endswith('.stl'):
o3d_mesh = o3d.io.read_triangle_mesh(path)
if not o3d_mesh.has_vertex_normals():
o3d_mesh.compute_vertex_normals()
mesh = Mesh(o3d_mesh=o3d_mesh)
self.meshes.append(mesh)
else:
self._create_cube()
def _create_cube(self):
mesh = Mesh.create_box()
self.meshes.append(mesh)
@staticmethod
def create_box(width=1.0, height=1.0, depth=1.0):
mesh = Mesh.create_box(width, height, depth)
return Model(mesh=mesh)
@staticmethod
def create_sphere(radius=1.0, resolution=20):
mesh = Mesh.create_sphere(radius, resolution)
return Model(mesh=mesh)
@staticmethod
def create_cylinder(radius=1.0, height=2.0, resolution=20, split=4):
mesh = Mesh.create_cylinder(radius, height, resolution, split)
return Model(mesh=mesh)
@staticmethod
def create_cone(radius=1.0, height=2.0, resolution=20, split=1):
mesh = Mesh.create_cone(radius, height, resolution, split)
return Model(mesh=mesh)
@staticmethod
def create_torus(torus_radius=1.0, tube_radius=0.2, radial_resolution=30, tubular_resolution=20):
mesh = Mesh.create_torus(torus_radius, tube_radius, radial_resolution, tubular_resolution)
return Model(mesh=mesh)
def set_color(self, color):
self.color = np.array(color, dtype=np.float32)
def set_shininess(self, shininess):
self.shininess = shininess
def draw(self, shader):
model_matrix = np.identity(4, dtype=np.float32)
model_matrix = pyrr.matrix44.create_from_scale(self.scale, dtype=np.float32)
rotation_x = pyrr.matrix44.create_from_x_rotation(np.radians(self.rotation[0]), dtype=np.float32)
rotation_y = pyrr.matrix44.create_from_y_rotation(np.radians(self.rotation[1]), dtype=np.float32)
rotation_z = pyrr.matrix44.create_from_z_rotation(np.radians(self.rotation[2]), dtype=np.float32)
rotation_matrix = pyrr.matrix44.multiply(rotation_x, rotation_y)
rotation_matrix = pyrr.matrix44.multiply(rotation_matrix, rotation_z)
model_matrix = pyrr.matrix44.multiply(model_matrix, rotation_matrix)
translation = pyrr.matrix44.create_from_translation(self.position, dtype=np.float32)
model_matrix = pyrr.matrix44.multiply(model_matrix, translation)
shader.set_mat4("model", model_matrix)
shader.set_float("material.shininess", self.shininess)
shader.set_vec3("objectColor", self.color)
for mesh in self.meshes:
has_textures = len(mesh.textures) > 0
shader.set_bool("hasTexture", has_textures)
mesh.draw(shader)
def set_position(self, position):
self.position = position
def set_rotation(self, rotation):
self.rotation = rotation
def set_scale(self, scale):
self.scale = scale
def rotate(self, axis, angle):
if axis[0] > 0:
self.rotation[0] += np.degrees(angle)
if axis[1] > 0:
self.rotation[1] += np.degrees(angle)
if axis[2] > 0:
self.rotation[2] += np.degrees(angle)

32
engine/obj_loader.py Normal file
View File

@@ -0,0 +1,32 @@
import numpy as np
import open3d as o3d
from .mesh import Mesh
from .texture import Texture
import os
def load_obj(file_path, directory=""):
mesh = o3d.io.read_triangle_mesh(file_path)
if not mesh.has_vertex_normals():
mesh.compute_vertex_normals()
textures = []
mtl_path = file_path.replace('.obj', '.mtl')
if os.path.exists(mtl_path):
with open(mtl_path, 'r') as f:
current_material = None
for line in f:
if line.startswith('newmtl'):
current_material = line.split()[1]
elif line.startswith('map_Kd') and current_material:
tex_path = os.path.join(directory, line.split()[1])
if os.path.exists(tex_path):
diffuse_texture = Texture(tex_path, "texture_diffuse")
textures.append(diffuse_texture)
elif line.startswith('map_Ks') and current_material:
tex_path = os.path.join(directory, line.split()[1])
if os.path.exists(tex_path):
specular_texture = Texture(tex_path, "texture_specular")
textures.append(specular_texture)
# Create and return the mesh with textures
return Mesh(o3d_mesh=mesh, textures=textures)

37
engine/scene.py Normal file
View File

@@ -0,0 +1,37 @@
from engine.light import PointLight
class Scene:
def __init__(self):
self.models = []
self.lights = []
self.camera = None
def add_model(self, model):
self.models.append(model)
def add_light(self, light):
self.lights.append(light)
def set_camera(self, camera):
self.camera = camera
def render(self, shader):
if not self.camera:
raise ValueError("Camera not set in scene")
#TODO: Actually do something with this boolean
dir_light_set = False
point_light_count = 0
for light in self.lights:
if isinstance(light, PointLight):
light.apply(shader, point_light_count)
point_light_count += 1
else:
light.apply(shader, 0)
dir_light_set = True
shader.set_int("numPointLights", point_light_count)
for model in self.models:
model.draw(shader)

69
engine/shader.py Normal file
View File

@@ -0,0 +1,69 @@
from OpenGL.GL import *
class Shader:
def __init__(self, vertex_path, fragment_path):
with open(vertex_path, 'r') as file:
vertex_source = file.read()
with open(fragment_path, 'r') as file:
fragment_source = file.read()
vertex_shader = self._compile_shader(vertex_source, GL_VERTEX_SHADER)
fragment_shader = self._compile_shader(fragment_source, GL_FRAGMENT_SHADER)
self.program = glCreateProgram()
glAttachShader(self.program, vertex_shader)
glAttachShader(self.program, fragment_shader)
glLinkProgram(self.program)
if not glGetProgramiv(self.program, GL_LINK_STATUS):
info_log = glGetProgramInfoLog(self.program)
glDeleteProgram(self.program)
glDeleteShader(vertex_shader)
glDeleteShader(fragment_shader)
raise RuntimeError(f"Shader program linking failed: {info_log}")
glDeleteShader(vertex_shader)
glDeleteShader(fragment_shader)
def _compile_shader(self, source, shader_type):
shader = glCreateShader(shader_type)
glShaderSource(shader, source)
glCompileShader(shader)
if not glGetShaderiv(shader, GL_COMPILE_STATUS):
info_log = glGetShaderInfoLog(shader)
glDeleteShader(shader)
shader_type_name = "vertex" if shader_type == GL_VERTEX_SHADER else "fragment"
raise RuntimeError(f"{shader_type_name} shader compilation failed: {info_log}")
return shader
def use(self):
glUseProgram(self.program)
def set_bool(self, name, value):
glUniform1i(glGetUniformLocation(self.program, name), int(value))
def set_int(self, name, value):
glUniform1i(glGetUniformLocation(self.program, name), value)
def set_float(self, name, value):
glUniform1f(glGetUniformLocation(self.program, name), value)
def set_vec2(self, name, value):
glUniform2fv(glGetUniformLocation(self.program, name), 1, value)
def set_vec3(self, name, value):
glUniform3fv(glGetUniformLocation(self.program, name), 1, value)
def set_vec4(self, name, value):
glUniform4fv(glGetUniformLocation(self.program, name), 1, value)
def set_mat2(self, name, value):
glUniformMatrix2fv(glGetUniformLocation(self.program, name), 1, GL_FALSE, value)
def set_mat3(self, name, value):
glUniformMatrix3fv(glGetUniformLocation(self.program, name), 1, GL_FALSE, value)
def set_mat4(self, name, value):
glUniformMatrix4fv(glGetUniformLocation(self.program, name), 1, GL_FALSE, value)

30
engine/texture.py Normal file
View File

@@ -0,0 +1,30 @@
from OpenGL.GL import *
from PIL import Image
import numpy as np
class Texture:
def __init__(self, path, type_name="texture_diffuse"):
self.id = glGenTextures(1)
self.type = type_name
self.path = path
image = Image.open(path)
image = image.transpose(Image.FLIP_TOP_BOTTOM)
img_data = np.array(list(image.getdata()), np.uint8)
if image.mode == "RGB":
img_format = GL_RGB
elif image.mode == "RGBA":
img_format = GL_RGBA
else:
raise ValueError(f"Unsupported image format: {image.mode}")
glBindTexture(GL_TEXTURE_2D, self.id)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(
GL_TEXTURE_2D, 0, img_format, image.width, image.height, 0,
img_format, GL_UNSIGNED_BYTE, img_data
)
glGenerateMipmap(GL_TEXTURE_2D)
image.close()

119
engine/window.py Normal file
View File

@@ -0,0 +1,119 @@
import glfw
from OpenGL.GL import *
class Window:
def __init__(self, width, height, title):
if not glfw.init():
raise Exception("Failed to initialize GLFW")
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
self.window = glfw.create_window(width, height, title, None, None)
if not self.window:
glfw.terminate()
raise Exception("Failed to create GLFW window")
glfw.make_context_current(self.window)
glfw.set_framebuffer_size_callback(self.window, self._framebuffer_size_callback)
glEnable(GL_DEPTH_TEST)
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK)
self.width = width
self.height = height
self.last_x = width / 2
self.last_y = height / 2
self.first_mouse = True
self.keys = {}
self.x_offset = 0
self.y_offset = 0
glfw.set_cursor_pos_callback(self.window, self._mouse_callback)
glfw.set_key_callback(self.window, self._key_callback)
glfw.set_scroll_callback(self.window, self._scroll_callback)
self.mouse_buttons = {}
glfw.set_mouse_button_callback(self.window, self._mouse_button_callback)
self.scroll_offset = 0
def _framebuffer_size_callback(self, window, width, height):
glViewport(0, 0, width, height)
self.width = width
self.height = height
def _mouse_callback(self, window, xpos, ypos):
if self.first_mouse:
self.last_x = xpos
self.last_y = ypos
self.first_mouse = False
self.x_offset = xpos - self.last_x
self.y_offset = self.last_y - ypos
self.last_x = xpos
self.last_y = ypos
def _key_callback(self, window, key, scancode, action, mods):
if action == glfw.PRESS:
self.keys[key] = True
elif action == glfw.RELEASE:
self.keys[key] = False
def _mouse_button_callback(self, window, button, action, mods):
if action == glfw.PRESS:
self.mouse_buttons[button] = True
elif action == glfw.RELEASE:
self.mouse_buttons[button] = False
def _scroll_callback(self, window, xoffset, yoffset):
self.scroll_offset = yoffset
def is_key_pressed(self, key):
return key in self.keys and self.keys[key]
def is_mouse_button_pressed(self, button):
return button in self.mouse_buttons and self.mouse_buttons[button]
def get_mouse_position(self):
return glfw.get_cursor_pos(self.window)
def get_mouse_offset(self):
offset = (self.x_offset, self.y_offset)
self.x_offset = 0
self.y_offset = 0
return offset
def get_scroll_offset(self):
offset = self.scroll_offset
self.scroll_offset = 0
return offset
def poll_events(self):
glfw.poll_events()
def should_close(self):
return glfw.window_should_close(self.window)
def set_should_close(self, value):
glfw.set_window_should_close(self.window, value)
def clear(self, r, g, b, a):
glClearColor(r, g, b, a)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
def swap_buffers(self):
glfw.swap_buffers(self.window)
def terminate(self):
glfw.terminate()
def get_aspect_ratio(self):
return self.width / self.height
def set_cursor_mode(self, mode):
glfw.set_input_mode(self.window, glfw.CURSOR, mode)

129
main.py Normal file
View File

@@ -0,0 +1,129 @@
import numpy as np
import os
import glfw
from engine.engine import Engine
from engine.camera import Camera
from engine.model import Model
from engine.light import DirectionalLight, PointLight
class GameApp(Engine):
def __init__(self, width=800, height=600, title="3D Game Demo"):
super().__init__(width, height, title)
self.camera = Camera(position=np.array([0, 2, 5]), target=np.array([0, 0, 0]))
self.scene.set_camera(self.camera)
self.camera_speed = 6
self.mouse_sensitivity = 0.2
self.window.set_cursor_mode(glfw.CURSOR_DISABLED)
dir_light = DirectionalLight(
direction=np.array([-0.2, -1.0, -0.3]),
ambient=np.array([0.2, 0.2, 0.2]),
diffuse=np.array([0.5, 0.5, 0.5]),
specular=np.array([1.0, 1.0, 1.0])
)
self.scene.add_light(dir_light)
point_light = PointLight(
position=np.array([1.0, 1.0, 1.0]),
ambient=np.array([0.1, 0.1, 0.1]),
diffuse=np.array([0.8, 0.8, 0.8]),
specular=np.array([1.0, 1.0, 1.0]),
constant=1.0,
linear=0.09,
quadratic=0.032
)
self.scene.add_light(point_light)
self.create_models()
def create_models(self):
cube_model = Model.create_box(1.0, 1.0, 1.0)
cube_model.set_position(np.array([-1.5, 0, 0]))
cube_model.set_color([0.8, 0.2, 0.2]) # Red
cube_model.set_shininess(64.0)
self.scene.add_model(cube_model)
self.cube_model = cube_model
sphere_model = Model.create_sphere(0.5, 32)
sphere_model.set_position(np.array([0, 0, 0]))
sphere_model.set_color([0.2, 0.8, 0.2]) # Green
sphere_model.set_shininess(128.0)
self.scene.add_model(sphere_model)
self.sphere_model = sphere_model
cylinder_model = Model.create_cylinder(0.5, 1.0, 32)
cylinder_model.set_position(np.array([1.5, 0, 0]))
cylinder_model.set_color([0.2, 0.2, 0.8]) # Blue
cylinder_model.set_shininess(32.0)
self.scene.add_model(cylinder_model)
self.cylinder_model = cylinder_model
assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
obj_path = os.path.join(assets_dir, "cube.obj")
if os.path.exists(obj_path):
try:
obj_model = Model(obj_path)
obj_model.set_position(np.array([0, 1.5, 0]))
obj_model.set_scale(np.array([0.5, 0.5, 0.5]))
self.scene.add_model(obj_model)
self.obj_model = obj_model
except Exception as e:
print(f"Could not load model: {e}")
else:
print(f"Model file not found: {obj_path}")
torus_model = Model.create_torus(0.5, 0.2, 30, 20)
torus_model.set_position(np.array([0, 1.5, 0]))
self.scene.add_model(torus_model)
self.torus_model = torus_model
torus_model.set_color([0.8, 0.8, 0.2])
torus_model.set_shininess(16.0)
def update(self, delta_time):
self.process_keyboard(delta_time)
self.process_mouse()
self.process_scroll()
self.cube_model.rotate(np.array([0, 1, 0]), 0.5 * delta_time)
self.sphere_model.rotate(np.array([1, 1, 0]), 0.3 * delta_time)
self.cylinder_model.rotate(np.array([0, 0, 1]), 0.7 * delta_time)
if self.window.is_key_pressed(glfw.KEY_ESCAPE):
self.window.set_should_close(True)
def process_keyboard(self, delta_time):
velocity = self.camera_speed * delta_time
if self.window.is_key_pressed(glfw.KEY_W):
self.camera.move("forward", velocity)
if self.window.is_key_pressed(glfw.KEY_S):
self.camera.move("backward", velocity)
if self.window.is_key_pressed(glfw.KEY_A):
self.camera.move("left", velocity)
if self.window.is_key_pressed(glfw.KEY_D):
self.camera.move("right", velocity)
if self.window.is_key_pressed(glfw.KEY_SPACE):
self.camera.move("up", velocity)
if self.window.is_key_pressed(glfw.KEY_LEFT_SHIFT):
self.camera.move("down", velocity)
def process_mouse(self):
x_offset, y_offset = self.window.get_mouse_offset()
if x_offset != 0 or y_offset != 0:
x_offset *= self.mouse_sensitivity
y_offset *= self.mouse_sensitivity
self.camera.rotate(x_offset, y_offset)
def process_scroll(self):
y_offset = self.window.get_scroll_offset()
if y_offset != 0:
current_zoom = self.camera.zoom
self.camera.set_zoom(current_zoom - y_offset)
def main():
game = GameApp(800, 600, "3D Game Engine Demo")
game.start()
if __name__ == "__main__":
main()

111
shaders/phong.frag Normal file
View File

@@ -0,0 +1,111 @@
#version 330 core
out vec4 FragColor;
struct Material {
sampler2D texture_diffuse1;
sampler2D texture_specular1;
float shininess;
};
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define MAX_POINT_LIGHTS 4
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform Material material;
uniform DirLight dirLight;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform int numPointLights;
// Function prototypes
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);
void main()
{
// Properties
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// Phase 1: Directional lighting
vec3 result = CalcDirLight(dirLight, norm, viewDir);
// Phase 2: Point lights
for(int i = 0; i < 1; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);
// If no texture is bound, use a default color
vec4 texColor = vec4(0.8, 0.8, 0.8, 1.0);
FragColor = vec4(result, 1.0) * texColor;
}
// Calculates the color when using a directional light
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// Diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// Specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
// Combine results
vec3 ambient = light.ambient;
vec3 diffuse = light.diffuse * diff;
vec3 specular = light.specular * spec;
return (ambient + diffuse + specular);
}
// Calculates the color when using a point light
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// Diffuse shading
float diff = max(dot(normal, lightDir), 0.0);
// Specular shading
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
// Attenuation
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance));
// Combine results
vec3 ambient = light.ambient;
vec3 diffuse = light.diffuse * diff;
vec3 specular = light.specular * spec;
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}

21
shaders/phong.vert Normal file
View File

@@ -0,0 +1,21 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoords = aTexCoords;
gl_Position = projection * view * vec4(FragPos, 1.0);
}