Engine Code
This commit is contained in:
38
assets/cube.obj
Normal file
38
assets/cube.obj
Normal 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
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)
|
||||||
129
main.py
Normal file
129
main.py
Normal 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
111
shaders/phong.frag
Normal 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
21
shaders/phong.vert
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user