diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b8670a85 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: + - repo: https://github.com/psf/black + rev: 21.9b0 + hooks: + - id: black + + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.5.7 + hooks: + - id: autopep8 + + - repo: https://github.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + + - repo: https://github.com/pre-commit/mirrors-pylint + rev: v2.7.4 + hooks: + - id: pylint + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: double-quote-string-fixer + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: mixed-line-ending + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: validate-commit-msg + - id: validate-pyproject-toml + - id: validate-xml + - id: validate-yaml +files: ^code/main.py$ \ No newline at end of file diff --git a/code/main.py b/code/main.py index 57efe9d5..72abd0d1 100644 --- a/code/main.py +++ b/code/main.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ Renders an image using raytracing """ import numpy as np import matplotlib.pyplot as plt @@ -6,18 +7,18 @@ IMAGE_WIDTH = 400 IMAGE_HEIGHT = 300 -def normalize(x): +def normalize(vector): """ Normalize a vector. Parameters: - x (numpy.ndarray): The input vector to be normalized. + vector (numpy.ndarray): The input vector to be normalized. Returns: numpy.ndarray: The normalized vector. """ - x /= np.linalg.norm(x) - return x + vector /= np.linalg.norm(vector) + return vector def intersect_plane(ray_origin, ray_direction, plane_point, plane_normal): @@ -71,6 +72,23 @@ def intersect_sphere(ray_origin, ray_direction, sphere_center, sphere_radius): radius_squared = sphere_radius * sphere_radius c = np.dot(origin_to_center, origin_to_center) - radius_squared disc = b * b - 4 * a * c + return calculate_sphere_intersection(a, b, c, disc) + + +def calculate_sphere_intersection(a, b, c, disc): + """ + Calculate the intersection distance of a ray with a sphere using the quadratic formula. + + Parameters: + a (float): Coefficient of t^2 in the quadratic equation. + b (float): Coefficient of t in the quadratic equation. + c (float): Constant term in the quadratic equation. + disc (float): Discriminant of the quadratic equation. + + Returns: + float: The distance from the origin to the intersection point with the sphere. + Returns +inf if there is no intersection or if the intersection is behind the origin. + """ if disc > 0: distance_squared = np.sqrt(disc) # q is used to find the roots of the quadratic equation @@ -166,41 +184,104 @@ def trace_ray(ray_origin, ray_direction): and the color at the intersection point. Returns None if there is no intersection. """ - # Find first point of intersection with the scene. + t, obj_idx = find_intersection(ray_origin, ray_direction) + if t == np.inf: + return + object_, intersection_point = get_intersection_details( + ray_origin, ray_direction, t, obj_idx) + normal, color = get_normal(object_, intersection_point), get_color( + object_, intersection_point) + if is_shadowed(intersection_point, normal, obj_idx): + return + return compute_color( + object_, intersection_point, normal, color, ray_origin) + + +def find_intersection(ray_origin, ray_direction): + """ + Find the intersection of a ray with the objects in the scene. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): + A normalized 3D vector representing the direction of the ray. + + Returns: + tuple: A tuple containing the distance to the intersection point + and the index of the intersected object. + """ t = np.inf obj_idx = -1 for index, object_ in enumerate(scene): t_obj = intersect(ray_origin, ray_direction, object_) if t_obj < t: t, obj_idx = t_obj, index - # Return None if the ray does not intersect any object. - if t == np.inf: - return - # Find the object. + return t, obj_idx + + +def get_intersection_details(ray_origin, ray_direction, t, obj_idx): + """ + Get the details of the intersection point on the object. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): + A normalized 3D vector representing the direction of the ray. + t (float): The distance to the intersection point. + obj_idx (int): The index of the intersected object in the scene. + + Returns: + tuple: A tuple containing the intersected object + and the intersection point. + """ object_ = scene[obj_idx] - # Find the point of intersection on the object. intersection_point = ray_origin + ray_direction * t - # Find properties of the object. - normal = get_normal(object_, intersection_point) - color = get_color(object_, intersection_point) + return object_, intersection_point + + +def is_shadowed(intersection_point, normal, obj_idx): + """ + Determine if the intersection point is in shadow. + + Parameters: + intersection_point (numpy.ndarray): + A 3D point representing the intersection point on the object. + normal (numpy.ndarray): The normal vector at the intersection point. + obj_idx (int): The index of the intersected object in the scene. + + Returns: + bool: True if the intersection point is in shadow, False otherwise. + """ to_light = normalize(L - intersection_point) - to_origin = normalize(O - intersection_point) - # Shadow: find if the point is shadowed or not. - shadow_intersections = [intersect( intersection_point + normal * .0001, - to_light, - obj_sh - ) + shadow_intersections = [intersect( + intersection_point + normal * .0001, to_light, obj_sh) for k, obj_sh in enumerate(scene) if k != obj_idx] - if shadow_intersections and min(shadow_intersections) < np.inf: - return - # Start computing the color. + return shadow_intersections and min(shadow_intersections) < np.inf + + +def compute_color(object_, intersection_point, normal, color, ray_origin): + """ + Compute the color at the intersection point using shading techniques. + + Parameters: + object_ (dict): A dictionary representing the intersected object. + intersection_point (numpy.ndarray): + A 3D point representing the intersection point on the object. + normal (numpy.ndarray): The normal vector at the intersection point. + color (numpy.ndarray): The base color of the object. + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + + Returns: + tuple: + A tuple containing the intersected object, intersection point, normal, + and the computed color. + """ + to_light = normalize(L - intersection_point) + to_origin = normalize(ray_origin - intersection_point) color_ray = ambient - # Lambert shading (diffuse). diffuse_intensity = object_.get('diffuse_c', diffuse_c) * max( np.dot(normal, to_light), 0) color_ray += diffuse_intensity * color - - # Blinn-Phong shading (specular). half_vector = normalize(to_light + to_origin) specular_intensity = object_.get('specular_c', specular_c) * max( np.dot(normal, half_vector), 0) ** specular_k diff --git a/code/requirements.txt b/code/requirements.txt index ac121f2d..4f6b1783 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -2,4 +2,8 @@ glfw numpy pyrr PyOpenGL -matplotlib \ No newline at end of file +matplotlib +flake8 +black +autopep8 +flake8-max-function-length \ No newline at end of file