From a7b1873e2a9c4814ebb1777a17bf0f5c1edb620f Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Sat, 20 Apr 2024 22:27:45 +0200 Subject: [PATCH] finished --- virtual_camera.py => 3d_renderer.py | 2 +- src/app.py | 190 +++++++++++++--------------- src/camera.py | 146 +++++++++++++++++++-- src/constants.py | 5 - src/object.py | 31 ++++- src/object_builder.py | 17 +-- src/renderer.py | 81 ++++++++++++ src/transformation.py | 24 ---- src/transformations.py | 42 ++++++ 9 files changed, 378 insertions(+), 160 deletions(-) rename virtual_camera.py => 3d_renderer.py (81%) delete mode 100644 src/constants.py create mode 100644 src/renderer.py delete mode 100644 src/transformation.py create mode 100644 src/transformations.py diff --git a/virtual_camera.py b/3d_renderer.py similarity index 81% rename from virtual_camera.py rename to 3d_renderer.py index 8f94055..4b586ac 100644 --- a/virtual_camera.py +++ b/3d_renderer.py @@ -2,4 +2,4 @@ from src.app import App if __name__ == "__main__": app = App() - app.main() \ No newline at end of file + app.main() diff --git a/src/app.py b/src/app.py index 4a67be9..a82ff45 100644 --- a/src/app.py +++ b/src/app.py @@ -1,115 +1,97 @@ -import pygame as pg -import numpy as np -from math import * -import src.constants as cn -import transformation as tm +from src.renderer import Renderer from src.object_builder import ObjectBuilder -from src.object import Object -from src.camera import Camera - - class App: - clock: pg.time.Clock - screen: pg.Surface - camera: Camera - objects = list[Object] + renderer : Renderer def __init__(self): - self.clock = pg.time.Clock() - self.screen = pg.display.set_mode(cn.WINDOW_SIZE) - self.objects = [] - self.camera = Camera(-5, 6, -55) - self.create_objects() - - def create_objects(self): - obj_builder = ObjectBuilder() - va = obj_builder.add_vertex(-1, -1, 1) - vb = obj_builder.add_vertex( 1, -1, 1) - vc = obj_builder.add_vertex( 1, 1, 1) - vd = obj_builder.add_vertex(-1, 1, 1) - ve = obj_builder.add_vertex(-1, -1, -1) - vf = obj_builder.add_vertex( 1, -1, -1) - vg = obj_builder.add_vertex( 1, 1, -1) - vh = obj_builder.add_vertex(-1, 1, -1) - obj_builder.add_vertices_connection(va, vb) - obj_builder.add_vertices_connection(va, vd) - obj_builder.add_vertices_connection(vd, vc) - obj_builder.add_vertices_connection(vb, vc) - obj_builder.add_vertices_connection(ve, vf) - obj_builder.add_vertices_connection(ve, vh) - obj_builder.add_vertices_connection(vh, vg) - obj_builder.add_vertices_connection(vf, vg) - obj_builder.add_vertices_connection(va, ve) - obj_builder.add_vertices_connection(vb, vf) - obj_builder.add_vertices_connection(vd, vh) - obj_builder.add_vertices_connection(vc, vg) - self.objects.append(obj_builder.build()) + self.renderer = Renderer(1280, 720) + self.renderer.caption = "Virtual camera" def main(self): - self.setup() - while True: - self.clock.tick(cn.FPS) - for event in pg.event.get(): - self.handle_event(event) - self.update() + obj_builder1 = ObjectBuilder() + va = obj_builder1.add_vertex(-1, 1, 1) + vb = obj_builder1.add_vertex( 1, 1, 1) + vc = obj_builder1.add_vertex( 1, 3, 1) + vd = obj_builder1.add_vertex(-1, 3, 1) + ve = obj_builder1.add_vertex(-1, 1, -1) + vf = obj_builder1.add_vertex( 1, 1, -1) + vg = obj_builder1.add_vertex( 1, 3, -1) + vh = obj_builder1.add_vertex(-1, 3, -1) + obj_builder1.add_vertices_connection(va, vb) + obj_builder1.add_vertices_connection(va, vd) + obj_builder1.add_vertices_connection(vd, vc) + obj_builder1.add_vertices_connection(vb, vc) + obj_builder1.add_vertices_connection(ve, vf) + obj_builder1.add_vertices_connection(ve, vh) + obj_builder1.add_vertices_connection(vh, vg) + obj_builder1.add_vertices_connection(vf, vg) + obj_builder1.add_vertices_connection(va, ve) + obj_builder1.add_vertices_connection(vb, vf) + obj_builder1.add_vertices_connection(vd, vh) + obj_builder1.add_vertices_connection(vc, vg) + self.renderer.add_object(obj_builder1.build()) - def handle_event(self, event: pg.event.Event): - match event.type: - case pg.QUIT: - self.quit() - case pg.KEYDOWN: - match event.key: - case pg.K_ESCAPE: - self.quit() - case pg.K_s: - self.camera.move_backward() - case pg.K_w: - self.camera.move_forward() - case pg.K_d: - self.camera.move_right() - case pg.K_a: - self.camera.move_left() - case pg.K_SPACE: - self.camera.move_up() - case pg.K_LSHIFT: - self.camera.move_down() - - def transform(self, transformation_matrix: np.matrix): - for key in self.points.keys(): - point = self.points[key] - self.points[key] = np.dot(transformation_matrix, point.reshape((3,1))) - - def setup(self): - pg.display.set_caption("Virtual camera") - - def update(self): - self.screen.fill((255,255,255)) - for obj in self.objects: - obj.draw() - pg.display.update() - - def quit(self): - pg.quit() - exit() + obj_builder2 = ObjectBuilder() + wa = obj_builder2.add_vertex(-2, 0, 2) + wb = obj_builder2.add_vertex( 2, 0, 2) + wc = obj_builder2.add_vertex( 2, 4, 2) + wd = obj_builder2.add_vertex(-2, 4, 2) + we = obj_builder2.add_vertex(-2, 0, -2) + wf = obj_builder2.add_vertex( 2, 0, -2) + wg = obj_builder2.add_vertex( 2, 4, -2) + wh = obj_builder2.add_vertex(-2, 4, -2) + obj_builder2.add_vertices_connection(wa, wb) + obj_builder2.add_vertices_connection(wa, wd) + obj_builder2.add_vertices_connection(wd, wc) + obj_builder2.add_vertices_connection(wb, wc) + obj_builder2.add_vertices_connection(we, wf) + obj_builder2.add_vertices_connection(we, wh) + obj_builder2.add_vertices_connection(wh, wg) + obj_builder2.add_vertices_connection(wf, wg) + obj_builder2.add_vertices_connection(wa, we) + obj_builder2.add_vertices_connection(wb, wf) + obj_builder2.add_vertices_connection(wd, wh) + obj_builder2.add_vertices_connection(wc, wg) + self.renderer.add_object(obj_builder2.build()) + obj_builder3 = ObjectBuilder() + ua = obj_builder3.add_vertex(-14, 0, 10) + ub = obj_builder3.add_vertex(-8, 0, 10) + uc = obj_builder3.add_vertex(-8, 8, 10) + ud = obj_builder3.add_vertex(-14, 8, 10) + ue = obj_builder3.add_vertex(-14, 0, -2) + uf = obj_builder3.add_vertex(-8, 0, -2) + ug = obj_builder3.add_vertex(-8, 8, -2) + uh = obj_builder3.add_vertex(-14, 8, -2) + obj_builder3.add_vertices_connection(ua, ub) + obj_builder3.add_vertices_connection(ua, ud) + obj_builder3.add_vertices_connection(ud, uc) + obj_builder3.add_vertices_connection(ub, uc) + obj_builder3.add_vertices_connection(ue, uf) + obj_builder3.add_vertices_connection(ue, uh) + obj_builder3.add_vertices_connection(uh, ug) + obj_builder3.add_vertices_connection(uf, ug) + obj_builder3.add_vertices_connection(ua, ue) + obj_builder3.add_vertices_connection(ub, uf) + obj_builder3.add_vertices_connection(ud, uh) + obj_builder3.add_vertices_connection(uc, ug) + self.renderer.add_object(obj_builder3.build()) -""" - projected_points = {} + obj_builder4 = ObjectBuilder() + za = obj_builder4.add_vertex(-2, 0, 10) + zb = obj_builder4.add_vertex( 2, 0, 10) + zc = obj_builder4.add_vertex( 2, 0, 6) + zd = obj_builder4.add_vertex(-2, 0, 6) + ze = obj_builder4.add_vertex( 0, 8, 8) + obj_builder4.add_vertices_connection(za, zb) + obj_builder4.add_vertices_connection(za, zd) + obj_builder4.add_vertices_connection(zd, zc) + obj_builder4.add_vertices_connection(zb, zc) + obj_builder4.add_vertices_connection(za, ze) + obj_builder4.add_vertices_connection(zb, ze) + obj_builder4.add_vertices_connection(zc, ze) + obj_builder4.add_vertices_connection(zd, ze) + self.renderer.add_object(obj_builder4.build()) - for point_key in self.points.keys(): - point = self.points[point_key] - projected_2D = np.dot(self.projection_matrix, point.reshape((3, 1))) - - x = int(projected_2D[0][0] * self.scale) + (1280 / 2) - y = int(projected_2D[1][0] * self.scale) + (720 / 2) - - pg.draw.circle(self.screen, (255, 0, 0), (x, y), 5) - - projected_points[point_key] = (x, y) - - for line in self.lines: - point_a = projected_points[line[0]] - point_b = projected_points[line[1]] - pg.draw.line(self.screen, (0,0,0), point_a, point_b) -""" \ No newline at end of file + self.renderer.run() \ No newline at end of file diff --git a/src/camera.py b/src/camera.py index 52a5e84..d83992d 100644 --- a/src/camera.py +++ b/src/camera.py @@ -1,30 +1,148 @@ import numpy as np -import transformation as tm +import math as mt +import src.transformations as tn class Camera: + __position: np.ndarray[float] + __yaw: float + __pitch: float + __roll: float + __right: np.ndarray[float] + __up: np.ndarray[float] + __forward: np.ndarray[float] + is_static: bool moving_speed: float rotation_speed: float - position: np.ndarray[float] + h_fov: float + v_fov: float + near_plane: float + far_plane: float - def __init__(self, x: float, y: int, z: int): + def __init__(self, renderer): + self.__renderer = renderer + self.__position = np.array([-5, 6, -55, 1.0]) + self.__yaw = 0 + self.__pitch = 0 + self.__roll = 0 + self.__right = np.array([1, 0, 0, 1]) + self.__up = np.array([0, 1, 0, 1]) + self.__forward = np.array([0, 0, 1, 1]) self.moving_speed = 0.3 self.rotation_speed = 0.015 - self.position = np.array([x, y, z, 1.0]) + self.is_static = False + self.near_plane = 0.1 + self.far_plane = 100 + self.set_fov(mt.pi / 3) - def move_forward(self): - self.position += tm.forward_array * self.moving_speed + def projection_matrix(self): + right = mt.tan(self.h_fov / 2) + top = mt.tan(self.v_fov / 2) - def move_backward(self): - self.position -= tm.forward_array * self.moving_speed + m00 = 1 / right + m11 = 1 / top + m22 = (self.far_plane + self.near_plane) / (self.far_plane - self.near_plane) + m32 = -2 * self.near_plane * self.far_plane / (self.far_plane - self.near_plane) - def move_right(self): - self.position += tm.right_array * self.moving_speed + matrix = np.matrix([ + [m00, 0, 0, 0], + [0, m11, 0, 0], + [0, 0, m22, 1], + [0, 0, m32, 0] + ]) + return matrix + + def to_screen_matrix(self): + hw = self.__renderer.window_width / 2 + hh = self.__renderer.window_height / 2 + + matrix = np.matrix([ + [hw, 0, 0, 0], + [0, -hh, 0, 0], + [0, 0, 1, 0], + [hw, hh, 0, 1] + ]) + return matrix + + def set_fov(self, h_fov: float): + self.h_fov = h_fov + self.v_fov = h_fov * (self.__renderer.window_height / self.__renderer.window_width) + + def set_position(self, x: float = None, y: float = None, z: float = None): + if x is not None: + self.__position[0] = x + if y is not None: + self.__position[1] = y + if z is not None: + self.__position[2] = z def move_left(self): - self.position -= tm.right_array * self.moving_speed + self.__position -= self.__right * self.moving_speed - def move_up(self): - self.position += tm.up_array * self.moving_speed + def move_right(self): + self.__position += self.__right * self.moving_speed def move_down(self): - self.position -= tm.up_array * self.moving_speed \ No newline at end of file + self.__position -= self.__up * self.moving_speed + + def move_up(self): + self.__position += self.__up * self.moving_speed + + def move_backward(self): + self.__position -= self.__forward * self.moving_speed + + def move_forward(self): + self.__position += self.__forward * self.moving_speed + + def yaw_down(self): + self.__yaw -= self.rotation_speed + + def yaw_up(self): + self.__yaw += self.rotation_speed + + def pitch_down(self): + self.__pitch -= self.rotation_speed + + def pitch_up(self): + self.__pitch += self.rotation_speed + + def roll_down(self): + self.__roll -= self.rotation_speed + + def roll_up(self): + self.__roll += self.rotation_speed + + def fov_up(self): + self.set_fov(self.h_fov - 0.05) + + def fov_down(self): + self.set_fov(self.h_fov + 0.05) + + def update_axis(self): + rotate = tn.rotate_x(self.__pitch) @ tn.rotate_y(self.__yaw) @ tn.rotate_z(self.__roll) + self.__forward = np.array([0, 0, 1, 1]) @ rotate + self.__right = np.array([1, 0, 0, 1]) @ rotate + self.__up = np.array([0, 1, 0, 1]) @ rotate + + def camera_matrix(self) -> np.matrix: + self.update_axis() + return self.translate_matrix() @ self.rotate_matrix() + + def translate_matrix(self) -> np.matrix: + x, y, z, w = self.__position + return np.matrix([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [-x, -y, -z, 1] + ]) + + def rotate_matrix(self) -> np.matrix: + rx, ry, rz, w = self.__right + fx, fy, fz, w = self.__forward + ux, uy, uz, w = self.__up + return np.matrix([ + [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/src/constants.py b/src/constants.py deleted file mode 100644 index 50a7584..0000000 --- a/src/constants.py +++ /dev/null @@ -1,5 +0,0 @@ -WINDOW_SIZE = (1280, 720) - -FPS = 60 -SCALE = 100 -ANGLE = 0.05 \ No newline at end of file diff --git a/src/object.py b/src/object.py index 09ac504..004e1fe 100644 --- a/src/object.py +++ b/src/object.py @@ -1,12 +1,35 @@ -from vertex import Vertex +import numpy as np +import pygame as pg +import os +from src.vertex import Vertex class Object: vertices: list[Vertex] - lines = list[(Vertex, Vertex)] + lines: list[(Vertex, Vertex)] def __init__(self, vertices: list[Vertex], lines: list[(Vertex, Vertex)]): self.vertices = vertices self.lines = lines - def draw(): - pass \ No newline at end of file + def attach_renderer(self, renderer): + self.__renderer = renderer + + def draw(self): + vertices_matrix = np.array([[v.x, v.y, v.z, 1.0] for v in self.vertices]) + vertices_matrix = vertices_matrix @ self.__renderer.camera.camera_matrix() + vertices_matrix = vertices_matrix @ self.__renderer.camera.projection_matrix() + vertices_matrix /= vertices_matrix[:, -1].reshape(-1, 1) + vertices_matrix = vertices_matrix @ self.__renderer.camera.to_screen_matrix() + vertices_matrix = vertices_matrix[:, :2] + vertices = {} + for i in range(len(self.vertices)): + vertices[self.vertices[i]] = vertices_matrix[i].tolist()[0] + + for v in vertices.values(): + pg.draw.circle(self.__renderer.screen, pg.Color("black"), v, 2) + for l in self.lines: + v1 = vertices[l[0]] + v2 = vertices[l[1]] + pg.draw.line(self.__renderer.screen, pg.Color("black"), v1, v2, 1) + + \ No newline at end of file diff --git a/src/object_builder.py b/src/object_builder.py index 7778ace..14d46bb 100644 --- a/src/object_builder.py +++ b/src/object_builder.py @@ -1,20 +1,21 @@ -from object import Object -from vertex import Vertex +from src.object import Object +from src.vertex import Vertex class ObjectBuilder: - vertices: list[Vertex] - lines: list[(Vertex, Vertex)] + __vertices: list[Vertex] + __lines: list[(Vertex, Vertex)] def __init__(self): - self.vertices = [] + self.__vertices = [] + self.__lines = [] def add_vertex(self, x: int, y: int, z: int) -> Vertex: v = Vertex(x, y, z) - self.vertices.append(v) + self.__vertices.append(v) return v def add_vertices_connection(self, vertex1: Vertex, vertex2: Vertex): - self.lines.append((vertex1, vertex2)) + self.__lines.append((vertex1, vertex2)) def build(self) -> Object: - pass \ No newline at end of file + return Object(self.__vertices, self.__lines) \ No newline at end of file diff --git a/src/renderer.py b/src/renderer.py new file mode 100644 index 0000000..194c150 --- /dev/null +++ b/src/renderer.py @@ -0,0 +1,81 @@ +import pygame as pg +import numpy as np +from math import * +from src.camera import Camera +from src.object import Object + +class Renderer: + __clock: pg.time.Clock + screen: pg.Surface + fps: int + caption: str + camera: Camera + window_width: int + window_height: int + objects: list[Object] + + def __init__(self, window_width: int, window_height: int): + self.__clock = pg.time.Clock() + self.screen = pg.display.set_mode((window_width, window_height)) + self.window_width = window_width + self.window_height = window_height + self.fps = 60 + self.caption = "window" + self.camera = Camera(self) + self.objects = [] + + def __quit(self): + pg.quit() + exit() + + def __handle_event(self, event: pg.event.Event): + if event.type == pg.QUIT: + self.__quit() + if not self.camera.is_static: + key = pg.key.get_pressed() + if key[pg.K_w]: + self.camera.move_forward() + if key[pg.K_s]: + self.camera.move_backward() + if key[pg.K_a]: + self.camera.move_left() + if key[pg.K_d]: + self.camera.move_right() + if key[pg.K_SPACE]: + self.camera.move_up() + if key[pg.K_LSHIFT]: + self.camera.move_down() + if key[pg.K_EQUALS]: + self.camera.fov_up() + if key[pg.K_MINUS]: + self.camera.fov_down() + if key[pg.K_r]: + self.camera.pitch_down() + if key[pg.K_f]: + self.camera.pitch_up() + if key[pg.K_q]: + self.camera.yaw_down() + if key[pg.K_e]: + self.camera.yaw_up() + if key[pg.K_z]: + self.camera.roll_down() + if key[pg.K_c]: + self.camera.roll_up() + + def __update(self): + self.screen.fill((255,255,255)) + for o in self.objects: + o.draw() + pg.display.update() + + def add_object(self, object: Object): + object.attach_renderer(self) + self.objects.append(object) + + def run(self): + pg.display.set_caption(self.caption) + while True: + for event in pg.event.get(): + self.__handle_event(event) + self.__update() + self.__clock.tick(self.fps) \ No newline at end of file diff --git a/src/transformation.py b/src/transformation.py deleted file mode 100644 index 448c84c..0000000 --- a/src/transformation.py +++ /dev/null @@ -1,24 +0,0 @@ -import numpy as np -from math import * - -rotation_x = lambda angle: np.matrix([ - [1, 0, 0 ], - [0, cos(angle), -sin(angle)], - [0, sin(angle), cos(angle) ], -]) - -rotation_y = lambda angle: np.matrix([ - [cos(angle), 0, sin(angle)], - [0, 1, 0 ], - [-sin(angle), 0, cos(angle)], -]) - -rotation_z = lambda angle: np.matrix([ - [cos(angle), -sin(angle), 0], - [sin(angle), cos(angle), 0], - [0, 0, 1], -]) - -forward_array = np.array([0, 0, 1, 1]) -up_array = np.array([0, 1, 0, 1]) -right_array = np.array([1, 0, 0, 1]) \ No newline at end of file diff --git a/src/transformations.py b/src/transformations.py new file mode 100644 index 0000000..f3aceb5 --- /dev/null +++ b/src/transformations.py @@ -0,0 +1,42 @@ +import math +import numpy as np + +def translate(x, y, z): + return np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [x, y, z, 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