diff --git a/LICENSE b/LICENSE index c12f423..1fc9bd5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 StanislavPetrovV +Copyright (c) 2025 tantock Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/__pycache__/camera.cpython-39.pyc b/__pycache__/camera.cpython-39.pyc deleted file mode 100644 index cccc386..0000000 Binary files a/__pycache__/camera.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/matrix_functions.cpython-39.pyc b/__pycache__/matrix_functions.cpython-39.pyc deleted file mode 100644 index 3901131..0000000 Binary files a/__pycache__/matrix_functions.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/object_3d.cpython-39.pyc b/__pycache__/object_3d.cpython-39.pyc deleted file mode 100644 index 60c5996..0000000 Binary files a/__pycache__/object_3d.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/projection.cpython-39.pyc b/__pycache__/projection.cpython-39.pyc deleted file mode 100644 index b78b7a1..0000000 Binary files a/__pycache__/projection.cpython-39.pyc and /dev/null differ diff --git a/camera.py b/camera.py deleted file mode 100644 index 384ef59..0000000 --- a/camera.py +++ /dev/null @@ -1,87 +0,0 @@ -import pygame as pg -from matrix_functions import * - -class Camera: - def __init__(self, render, position): - self.render = render - self.position = np.array([*position, 1.0]) - self.forward = np.array([0, 0, 1, 1]) - self.up = np.array([0, 1, 0, 1]) - self.right = np.array([1, 0, 0, 1]) - self.h_fov = math.pi / 3 - self.v_fov = self.h_fov * (render.HEIGHT / render.WIDTH) - self.near_plane = 0.1 - self.far_plane = 100 - self.moving_speed = 0.3 - self.rotation_speed = 0.015 - - self.anglePitch = 0 - self.angleYaw = 0 - self.angleRoll = 0 - - def control(self): - key = pg.key.get_pressed() - if key[pg.K_a]: - self.position -= self.right * self.moving_speed - if key[pg.K_d]: - self.position += self.right * self.moving_speed - if key[pg.K_w]: - self.position += self.forward * self.moving_speed - if key[pg.K_s]: - self.position -= self.forward * self.moving_speed - if key[pg.K_q]: - self.position += self.up * self.moving_speed - if key[pg.K_e]: - self.position -= self.up * self.moving_speed - - if key[pg.K_LEFT]: - self.camera_yaw(-self.rotation_speed) - if key[pg.K_RIGHT]: - self.camera_yaw(self.rotation_speed) - if key[pg.K_UP]: - self.camera_pitch(-self.rotation_speed) - if key[pg.K_DOWN]: - self.camera_pitch(self.rotation_speed) - - def camera_yaw(self, angle): - self.angleYaw += angle - - def camera_pitch(self, angle): - self.anglePitch += angle - - def axiiIdentity(self): - self.forward = np.array([0, 0, 1, 1]) - self.up = np.array([0, 1, 0, 1]) - self.right = np.array([1, 0, 0, 1]) - - def camera_update_axii(self): - # rotate = rotate_y(self.angleYaw) @ rotate_x(self.anglePitch) - rotate = rotate_x(self.anglePitch) @ rotate_y(self.angleYaw) # this concatenation gives right visual - self.axiiIdentity() - self.forward = self.forward @ rotate - self.right = self.right @ rotate - self.up = self.up @ rotate - - def camera_matrix(self): - self.camera_update_axii() - return self.translate_matrix() @ self.rotate_matrix() - - def translate_matrix(self): - x, y, z, w = self.position - return np.array([ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [-x, -y, -z, 1] - ]) - - def rotate_matrix(self): - rx, ry, rz, w = self.right - fx, fy, fz, w = self.forward - ux, uy, uz, w = self.up - return np.array([ - [rx, ux, fx, 0], - [ry, uy, fy, 0], - [rz, uz, fz, 0], - [0, 0, 0, 1] - ]) \ No newline at end of file diff --git a/main.py b/main.py index 2bdb689..388ee9c 100644 --- a/main.py +++ b/main.py @@ -1,50 +1,15 @@ -from object_3d import * -from camera import * -from projection import * -import pygame as pg - - -class SoftwareRender: - def __init__(self): - pg.init() - self.RES = self.WIDTH, self.HEIGHT = 1600, 900 - self.H_WIDTH, self.H_HEIGHT = self.WIDTH // 2, self.HEIGHT // 2 - self.FPS = 60 - self.screen = pg.display.set_mode(self.RES) - self.clock = pg.time.Clock() - self.create_objects() - - def create_objects(self): - self.camera = Camera(self, [-5, 6, -55]) - self.projection = Projection(self) - self.object = self.get_object_from_file('resources/t_34_obj.obj') - self.object.rotate_y(-math.pi / 4) - - def get_object_from_file(self, filename): - vertex, faces = [], [] - with open(filename) as f: - for line in f: - if line.startswith('v '): - vertex.append([float(i) for i in line.split()[1:]] + [1]) - elif line.startswith('f'): - faces_ = line.split()[1:] - faces.append([int(face_.split('/')[0]) - 1 for face_ in faces_]) - return Object3D(self, vertex, faces) - - def draw(self): - self.screen.fill(pg.Color('darkslategray')) - self.object.draw() - - def run(self): - while True: - self.draw() - self.camera.control() - [exit() for i in pg.event.get() if i.type == pg.QUIT] - pg.display.set_caption(str(self.clock.get_fps())) - pg.display.flip() - self.clock.tick(self.FPS) - +from obj4drender.render import SoftwareRender +from obj4drender.object import Axes4 +import math if __name__ == '__main__': app = SoftwareRender() + tesseract_id = app.load_object_from_file('resources/tesseract.obj4') + tank_id = app.load_object_from_file('resources/t_34_obj.obj') + axis_id = app.add_object(Axes4(app)) + app.get_object(tesseract_id).rotate_y(-math.pi / 4) + app.get_object(tesseract_id).scale(10) + app.get_object(tesseract_id).translate([10,10,10,0]) + app.get_object(tank_id).rotate_y(-math.pi / 4) + app.get_object(tank_id).scale(1/5) app.run() \ No newline at end of file diff --git a/matrix_functions.py b/matrix_functions.py deleted file mode 100644 index 21aab07..0000000 --- a/matrix_functions.py +++ /dev/null @@ -1,48 +0,0 @@ -import math -import numpy as np - - -def translate(pos): - tx, ty, tz = pos - return np.array([ - [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [tx, ty, tz, 1] - ]) - - -def rotate_x(a): - return np.array([ - [1, 0, 0, 0], - [0, math.cos(a), math.sin(a), 0], - [0, -math.sin(a), math.cos(a), 0], - [0, 0, 0, 1] - ]) - - -def rotate_y(a): - return np.array([ - [math.cos(a), 0, -math.sin(a), 0], - [0, 1, 0, 0], - [math.sin(a), 0, math.cos(a), 0], - [0, 0, 0, 1] - ]) - - -def rotate_z(a): - return np.array([ - [math.cos(a), math.sin(a), 0, 0], - [-math.sin(a), math.cos(a), 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1] - ]) - - -def scale(n): - return np.array([ - [n, 0, 0, 0], - [0, n, 0, 0], - [0, 0, n, 0], - [0, 0, 0, 1] - ]) \ No newline at end of file diff --git a/projection.py b/projection.py deleted file mode 100644 index 408af86..0000000 --- a/projection.py +++ /dev/null @@ -1,31 +0,0 @@ -import math -import numpy as np - - -class Projection: - def __init__(self, render): - NEAR = render.camera.near_plane - FAR = render.camera.far_plane - RIGHT = math.tan(render.camera.h_fov / 2) - LEFT = -RIGHT - TOP = math.tan(render.camera.v_fov / 2) - BOTTOM = -TOP - - m00 = 2 / (RIGHT - LEFT) - m11 = 2 / (TOP - BOTTOM) - m22 = (FAR + NEAR) / (FAR - NEAR) - m32 = -2 * NEAR * FAR / (FAR - NEAR) - self.projection_matrix = np.array([ - [m00, 0, 0, 0], - [0, m11, 0, 0], - [0, 0, m22, 1], - [0, 0, m32, 0] - ]) - - HW, HH = render.H_WIDTH, render.H_HEIGHT - self.to_screen_matrix = np.array([ - [HW, 0, 0, 0], - [0, -HH, 0, 0], - [0, 0, 1, 0], - [HW, HH, 0, 1] - ]) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0a59e97 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "obj4drender" +version = "0.0.1" +dependencies = ["pygame","numpy","numba"] +requires-python = ">=3.10" +classifiers = ["License :: OSI Approved :: MIT License"] +readme = "README.md" + +[project.optional-dependencies] +dev = ["pytest", "coverage"] + +[build-system] +requires = [ + "wheel", + "setuptools-scm[toml]>=8.0", +] +build-backend = "setuptools.build_meta" + +[project.urls] +Homepage = "https://github.com/tantock/Software_3D_engine" + diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2162b83 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +build +setuptools +setuptools_scm +wheel \ No newline at end of file diff --git a/resources/tesseract.obj4 b/resources/tesseract.obj4 new file mode 100644 index 0000000..8c9133f --- /dev/null +++ b/resources/tesseract.obj4 @@ -0,0 +1,81 @@ +# +# object tesseract +# + +v 0.0000 0.0000 0.0000 0.0000 +v 0.0000 1.0000 0.0000 0.0000 +v 1.0000 1.0000 0.0000 0.0000 +v 1.0000 0.0000 0.0000 0.0000 +v 0.0000 0.0000 1.0000 0.0000 +v 0.0000 1.0000 1.0000 0.0000 +v 1.0000 1.0000 1.0000 0.0000 +v 1.0000 0.0000 1.0000 0.0000 +v 0.0000 0.0000 0.0000 1.0000 +v 0.0000 1.0000 0.0000 1.0000 +v 1.0000 1.0000 0.0000 1.0000 +v 1.0000 0.0000 0.0000 1.0000 +v 0.0000 0.0000 1.0000 1.0000 +v 0.0000 1.0000 1.0000 1.0000 +v 1.0000 1.0000 1.0000 1.0000 +v 1.0000 0.0000 1.0000 1.0000 +# 16 vertices + +f 1 2 3 4 +f 3 4 8 7 +f 2 3 7 6 +f 1 2 6 5 +f 1 4 8 5 +f 5 8 7 6 +f 9 10 11 12 +f 11 12 16 15 +f 10 11 15 14 +f 9 10 14 13 +f 9 12 16 13 +f 13 16 15 14 +f 3 4 8 7 +f 8 7 15 16 +f 4 8 16 12 +f 3 4 12 11 +f 3 7 15 11 +f 11 15 16 12 +f 1 2 3 4 +f 3 4 12 11 +f 2 3 11 10 +f 1 2 10 9 +f 1 4 12 9 +f 9 12 11 10 +f 2 3 7 6 +f 7 6 14 15 +f 3 7 15 11 +f 2 3 11 10 +f 2 6 14 10 +f 10 14 15 11 +f 1 2 6 5 +f 6 5 13 14 +f 2 6 14 10 +f 1 2 10 9 +f 1 5 13 9 +f 9 13 14 10 +f 5 6 7 8 +f 7 8 16 15 +f 6 7 15 14 +f 5 6 14 13 +f 5 8 16 13 +f 13 16 15 14 +f 1 4 8 5 +f 8 5 13 16 +f 4 8 16 12 +f 1 4 12 9 +f 1 5 13 9 +f 9 13 16 12 + +c 1 2 3 4 5 6 +c 7 8 9 10 11 12 +c 13 14 15 16 17 18 +c 19 20 21 22 23 24 +c 25 26 27 28 29 30 +c 31 32 33 34 35 36 +c 37 38 39 40 41 42 +c 43 44 45 46 47 48 + +# 8 cells \ No newline at end of file diff --git a/src/obj4drender/__init__.py b/src/obj4drender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/obj4drender/camera.py b/src/obj4drender/camera.py new file mode 100644 index 0000000..61e615b --- /dev/null +++ b/src/obj4drender/camera.py @@ -0,0 +1,137 @@ +import pygame as pg +from src.obj4drender.matrix_functions import * + +class Camera: + def __init__(self, render, position): + self.render = render + self.position = np.array([*position, 1.0]) + self.forward = np.array([0, 0, 1, 0, 1]) + self.up = np.array([0, 1, 0, 0, 1]) + self.right = np.array([1, 0, 0, 0, 1]) + self.there = np.array([0, 0, 0, 1, 1]) + self.h_fov = math.pi / 3 + self.v_fov = self.h_fov * (render.HEIGHT / render.WIDTH) + self.d_fov = self.h_fov * (render.DEPTH / render.WIDTH) + self.near_plane = 0.1 + self.far_plane = 100 + self.moving_speed = 0.3 + self.rotation_speed = 0.015 + + self.anglePitch = 0 + self.angleYaw = 0 + self.angleRoll = 0 + + self.angleYawRoll = 0 #yz + self.anglePitchRoll = 0 #xz + self.anglePitchYaw = 0 #xy + self.four_rotation = False + + def control(self): + key = pg.key.get_pressed() + if key[pg.K_a]: + self.position -= self.right * self.moving_speed + if key[pg.K_d]: + self.position += self.right * self.moving_speed + if key[pg.K_w]: + self.position += self.forward * self.moving_speed + if key[pg.K_s]: + self.position -= self.forward * self.moving_speed + if key[pg.K_q]: + self.position += self.up * self.moving_speed + if key[pg.K_e]: + self.position -= self.up * self.moving_speed + if key[pg.K_SPACE]: + self.position += self.there * self.moving_speed + if key[pg.K_LCTRL]: + self.position -= self.there * self.moving_speed + + if key[pg.K_LEFT]: + self.four_rotation = False + self.camera_yaw(-self.rotation_speed) + if key[pg.K_RIGHT]: + self.four_rotation = False + self.camera_yaw(self.rotation_speed) + if key[pg.K_UP]: + self.four_rotation = False + self.camera_pitch(-self.rotation_speed) + if key[pg.K_DOWN]: + self.four_rotation = False + self.camera_pitch(self.rotation_speed) + if key[pg.K_i]: + self.four_rotation = True + self.camera_PitchYaw(-self.rotation_speed) + if key[pg.K_k]: + self.four_rotation = True + self.camera_PitchYaw(self.rotation_speed) + if key[pg.K_j]: + self.four_rotation = True + self.camera_YawRoll(-self.rotation_speed) + if key[pg.K_l]: + self.four_rotation = True + self.camera_YawRoll(self.rotation_speed) + if key[pg.K_u]: + self.four_rotation = True + self.camera_PitchRoll(-self.rotation_speed) + if key[pg.K_o]: + self.four_rotation = True + self.camera_PitchRoll(self.rotation_speed) + + def camera_yaw(self, angle): + self.angleYaw += angle + + def camera_pitch(self, angle): + self.anglePitch += angle + + def camera_YawRoll(self, angle): #yz + self.angleYawRoll += angle + + def camera_PitchRoll(self, angle): #xz + self.anglePitchRoll += angle + + def camera_PitchYaw(self, angle): #xy + self.anglePitchYaw += angle + + def axiiIdentity(self): + self.forward = np.array([0, 0, 1, 0, 1]) + self.up = np.array([0, 1, 0, 0, 1]) + self.right = np.array([1, 0, 0, 0, 1]) + self.there = np.array([0, 0, 0, 1, 1]) + + def camera_update_axii(self): + # rotate = rotate_y(self.angleYaw) @ rotate_x(self.anglePitch) + if not self.four_rotation: + rotate = rotate_x(self.anglePitch) @ rotate_y(self.angleYaw) # this concatenation gives right visual + else: + rotate = rotate_xy(self.anglePitchYaw) @ rotate_yz(self.angleYawRoll) @ rotate_xz(self.anglePitchRoll) + self.axiiIdentity() + self.forward = self.forward @ rotate + self.right = self.right @ rotate + self.up = self.up @ rotate + self.there = self.there @ rotate + + def camera_matrix(self): + self.camera_update_axii() + return self.translate_matrix() @ self.rotate_matrix() + + def translate_matrix(self): + x, y, z, w, h = self.position + return np.array([ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [-x, -y, -z, -w, 1] + ]) + + def rotate_matrix(self): + rx, ry, rz, rw, h = self.right + fx, fy, fz, fw, h = self.forward + ux, uy, uz, uw, h = self.up + tx, ty, tz, tw, h = self.there + return np.array([ + [rx, ux, fx, tx, 0], + [ry, uy, fy, ty, 0], + [rz, uz, fz, tz, 0], + [rw, uw, fw, tw, 0], + [0, 0, 0, 0, 1] + ]) \ No newline at end of file diff --git a/src/obj4drender/matrix_functions.py b/src/obj4drender/matrix_functions.py new file mode 100644 index 0000000..fd8073f --- /dev/null +++ b/src/obj4drender/matrix_functions.py @@ -0,0 +1,107 @@ +import math +import numpy as np + + +def translate(pos): + tx, ty, tz, tw = pos + return np.array([ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [tx, ty, tz, tw, 1] + ]) + + +def rotate_x(a): + return np.array([ + [1, 0, 0, 0, 0], + [0, math.cos(a), math.sin(a), 0, 0], + [0, -math.sin(a), math.cos(a), 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + + +def rotate_y(a): + return np.array([ + [math.cos(a), 0, -math.sin(a), 0, 0], + [0, 1, 0, 0, 0], + [math.sin(a), 0, math.cos(a), 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + + +def rotate_z(a): + return np.array([ + [math.cos(a), math.sin(a), 0, 0, 0], + [-math.sin(a), math.cos(a), 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + + +def scale(n): + return np.array([ + [n, 0, 0, 0, 0], + [0, n, 0, 0, 0], + [0, 0, n, 0, 0], + [0, 0, 0, n, 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_zw(a): + return np.array([ + [math.cos(a), math.sin(a), 0, 0, 0], + [-math.sin(a), math.cos(a), 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_yw(a): + return np.array([ + [math.cos(a), 0, math.sin(a), 0, 0], + [0, 1, 0, 0, 0], + [-math.sin(a), 0, math.cos(a), 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_yz(a): + return np.array([ + [math.cos(a), 0, 0, math.sin(a), 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [-math.sin(a), 0, 0, math.cos(a), 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_xw(a): + return np.array([ + [1, 0, 0, 0, 0], + [0, math.cos(a), math.sin(a), 0, 0], + [0, -math.sin(a), math.cos(a), 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_xz(a): + return np.array([ + [1, 0, 0, 0, 0], + [0, math.cos(a), 0, math.sin(a), 0], + [0, 0, 1, 0, 0], + [0, -math.sin(a), 0, math.cos(a), 0], + [0, 0, 0, 0, 1] + ]) + +def rotate_xy(a): + return np.array([ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, math.cos(a), math.sin(a), 0], + [0, 0, -math.sin(a), math.cos(a), 0], + [0, 0, 0, 0, 1] + ]) \ No newline at end of file diff --git a/object_3d.py b/src/obj4drender/object.py similarity index 70% rename from object_3d.py rename to src/obj4drender/object.py index 031f624..5296ff8 100644 --- a/object_3d.py +++ b/src/obj4drender/object.py @@ -1,32 +1,27 @@ import pygame as pg -from matrix_functions import * +from src.obj4drender.matrix_functions import * from numba import njit @njit(fastmath=True) -def any_func(arr, a, b): - return np.any((arr == a) | (arr == b)) +def any_func(arr, a, b, c): + return np.any((arr == a) | (arr == b) | (arr == c)) -class Object3D: +class Object: def __init__(self, render, vertices='', faces=''): self.render = render self.vertices = np.array(vertices) self.faces = faces - self.translate([0.0001, 0.0001, 0.0001]) + self.translate([0.0001, 0.0001, 0.0001, 0.0001]) self.font = pg.font.SysFont('Arial', 30, bold=True) self.color_faces = [(pg.Color('orange'), face) for face in self.faces] - self.movement_flag, self.draw_vertices = True, False + self.draw_vertices = False self.label = '' def draw(self): self.screen_projection() - self.movement() - - def movement(self): - if self.movement_flag: - self.rotate_y(-(pg.time.get_ticks() % 0.005)) def screen_projection(self): vertices = self.vertices @ self.render.camera.camera_matrix() @@ -39,7 +34,7 @@ def screen_projection(self): for index, color_face in enumerate(self.color_faces): color, face = color_face polygon = vertices[face] - if not any_func(polygon, self.render.H_WIDTH, self.render.H_HEIGHT): + if not any_func(polygon, self.render.H_WIDTH, self.render.H_HEIGHT, self.render.H_DEPTH): pg.draw.polygon(self.render.screen, color, polygon, 1) if self.label: text = self.font.render(self.label[index], True, pg.Color('white')) @@ -47,7 +42,7 @@ def screen_projection(self): if self.draw_vertices: for vertex in vertices: - if not any_func(vertex, self.render.H_WIDTH, self.render.H_HEIGHT): + if not any_func(vertex, self.render.H_WIDTH, self.render.H_HEIGHT, self.render.H_DEPTH): pg.draw.circle(self.render.screen, pg.Color('white'), vertex, 2) def translate(self, pos): @@ -66,12 +61,20 @@ def rotate_z(self, angle): self.vertices = self.vertices @ rotate_z(angle) -class Axes(Object3D): +class Axes3(Object): def __init__(self, render): - super().__init__(render) - self.vertices = np.array([(0, 0, 0, 1), (1, 0, 0, 1), (0, 1, 0, 1), (0, 0, 1, 1)]) + super().__init__(render,vertices=np.array([(0, 0, 0, 0, 1), (1, 0, 0, 0, 1), (0, 1, 0, 0, 1), (0, 0, 1, 0, 1)])) self.faces = np.array([(0, 1), (0, 2), (0, 3)]) self.colors = [pg.Color('red'), pg.Color('green'), pg.Color('blue')] self.color_faces = [(color, face) for color, face in zip(self.colors, self.faces)] self.draw_vertices = False self.label = 'XYZ' + +class Axes4(Object): + def __init__(self, render): + super().__init__(render,vertices=np.array([(0, 0, 0, 0, 1), (1, 0, 0, 0, 1), (0, 1, 0, 0, 1), (0, 0, 1, 0, 1), (0, 0, 0, 1, 1)])) + self.faces = np.array([(0, 1), (0, 2), (0, 3), (0, 4)]) + self.colors = [pg.Color('red'), pg.Color('green'), pg.Color('blue'), pg.Color('yellow')] + self.color_faces = [(color, face) for color, face in zip(self.colors, self.faces)] + self.draw_vertices = False + self.label = 'XYZW' \ No newline at end of file diff --git a/src/obj4drender/projection.py b/src/obj4drender/projection.py new file mode 100644 index 0000000..dd6e139 --- /dev/null +++ b/src/obj4drender/projection.py @@ -0,0 +1,36 @@ +import math +import numpy as np + + +class Projection: + def __init__(self, render): + NEAR = render.camera.near_plane + FAR = render.camera.far_plane + RIGHT = math.tan(render.camera.h_fov / 2) + LEFT = -RIGHT + TOP = math.tan(render.camera.v_fov / 2) + BOTTOM = -TOP + THERE = math.tan(render.camera.d_fov / 2) + HERE = -THERE + + m00 = 2 / (RIGHT - LEFT) + m11 = 2 / (TOP - BOTTOM) + m22 = 2 / (THERE - HERE) + m33 = (FAR + NEAR) / (FAR - NEAR) + m43 = -2 * NEAR * FAR / (FAR - NEAR) + self.projection_matrix = np.array([ + [m00, 0, 0, 0, 0], + [0, m11, 0, 0, 0], + [0, 0, m22, 0, 1], + [0, 0, 0, m33, 1], + [0, 0, 0, m43, 0] + ]) + + HW, HH, HD = render.H_WIDTH, render.H_HEIGHT, render.H_DEPTH + self.to_screen_matrix = np.array([ + [HW, 0, 0, 0, 0], + [0, -HH, 0, 0, 0], + [0, 0, HD, 0, 0], + [0, 0, 0, 1, 0], + [HW, HH, HD, 0, 1] + ]) \ No newline at end of file diff --git a/src/obj4drender/render.py b/src/obj4drender/render.py new file mode 100644 index 0000000..5fc14fe --- /dev/null +++ b/src/obj4drender/render.py @@ -0,0 +1,64 @@ +from src.obj4drender.object import * +from src.obj4drender.camera import * +from src.obj4drender.projection import * +import pygame as pg + +class SoftwareRender: + def __init__(self): + pg.init() + self.RES = self.WIDTH, self.HEIGHT = 1600, 900 + self.DEPTH = 1600 + self.H_WIDTH, self.H_HEIGHT, self.H_DEPTH = self.WIDTH // 2, self.HEIGHT // 2, self.DEPTH //2 + self.FPS = 60 + self.screen = pg.display.set_mode(self.RES) + self.clock = pg.time.Clock() + self.objects = {} + self.autoincrement_obj_id = 0 + self.create_default_scene() + + def create_default_scene(self): + self.camera = Camera(self, [-5, 6, -55, 0]) + self.projection = Projection(self) + + def load_object_from_file(self, filename) -> int: + return self.add_object(self.get_object_from_file(filename)) + + def add_object(self, object:Object) -> int: + id = self.autoincrement_obj_id + self.objects[id] = object + self.autoincrement_obj_id += 1 + return id + + def pop_object(self, id:int) -> Object: + return self.objects.pop(id) + + def get_object(self, id:int) -> Object: + return self.objects[id] + + def get_object_from_file(self, filename): + vertex, faces = [], [] + extra_dim = [0, 1] + if filename[-5:] == '.obj4': + extra_dim = [1] + with open(filename) as f: + for line in f: + if line.startswith('v '): + vertex.append([float(i) for i in line.split()[1:]] + extra_dim) + elif line.startswith('f'): + faces_ = line.split()[1:] + faces.append([int(face_.split('/')[0]) - 1 for face_ in faces_]) + return Object(self, vertex, faces) + + def draw(self): + self.screen.fill(pg.Color('darkslategray')) + for object in self.objects.values(): + object.draw() + + def run(self): + while True: + self.draw() + self.camera.control() + [exit() for i in pg.event.get() if i.type == pg.QUIT] + pg.display.set_caption(str(self.clock.get_fps())) + pg.display.flip() + self.clock.tick(self.FPS) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test_matrix_functions.py b/tests/test_matrix_functions.py new file mode 100644 index 0000000..6220618 --- /dev/null +++ b/tests/test_matrix_functions.py @@ -0,0 +1,40 @@ +from src.obj4drender.matrix_functions import * + +def init_vector(): + return np.array([1,2,3,4,1]) #x,y,z,w,h +def test_translate(): + vec = init_vector() + np.testing.assert_array_equal(vec @ translate([0,0,0,0]), init_vector()) + np.testing.assert_array_equal(vec @ translate([5,1,2,8]), np.array([6,3,5,12,1])) + np.testing.assert_array_equal(vec @ translate([-5,-1,-2,-8]), np.array([-4,1,1,-4,1])) + +def test_scale(): + vec = init_vector() + np.testing.assert_array_equal(vec @ scale(1), init_vector()) + np.testing.assert_array_equal(vec @ scale(2), np.array([2,4,6,8,1])) + +def test_rotate(): + vec = init_vector() + np.testing.assert_array_almost_equal(vec @ rotate_zw(0), init_vector()) + + np.testing.assert_array_almost_equal(vec @ rotate_zw(np.pi/2), np.array([-2,1,3,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yw(np.pi/2), np.array([-3,2,1,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yz(np.pi/2), np.array([-4,2,3,1,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xw(np.pi/2), np.array([1,-3,2,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xz(np.pi/2), np.array([1,-4,3,2,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xy(np.pi/2), np.array([1,2,-4,3,1])) + + + np.testing.assert_array_almost_equal(vec @ rotate_zw(-np.pi/2), np.array([2,-1,3,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yw(-np.pi/2), np.array([3,2,-1,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yz(-np.pi/2), np.array([4,2,3,-1,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xw(-np.pi/2), np.array([1,3,-2,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xz(-np.pi/2), np.array([1,4,3,-2,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xy(-np.pi/2), np.array([1,2,4,-3,1])) + + np.testing.assert_array_almost_equal(vec @ rotate_zw(np.pi), np.array([-1,-2,3,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yw(np.pi), np.array([-1,2,-3,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_yz(np.pi), np.array([-1,2,3,-4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xw(np.pi), np.array([1,-2,-3,4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xz(np.pi), np.array([1,-2,3,-4,1])) + np.testing.assert_array_almost_equal(vec @ rotate_xy(np.pi), np.array([1,2,-3,-4,1])) \ No newline at end of file