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

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)