Engine Code
This commit is contained in:
0
engine/__init__.py
Normal file
0
engine/__init__.py
Normal file
85
engine/camera.py
Normal file
85
engine/camera.py
Normal 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
75
engine/engine.py
Normal 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
65
engine/light.py
Normal 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
128
engine/mesh.py
Normal 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
91
engine/mesh_utils.py
Normal 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
117
engine/model.py
Normal 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
32
engine/obj_loader.py
Normal 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
37
engine/scene.py
Normal 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
69
engine/shader.py
Normal 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
30
engine/texture.py
Normal 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
119
engine/window.py
Normal 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)
|
||||
Reference in New Issue
Block a user