diff --git a/README.md b/README.md index a011c8c8..fb3e03a2 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,8 @@ python main.py --algorithm ray_tracing # Wywołanie algorytmu ray tracing ze specyfikacją sceny z folderu scenes, liczbą sampli na pixek, rozdzielczością, środowiskiem i rozmyciem środowiska python main.py --scene three_spheres --samples_per_pixel 100 --resolution 100x100 --environment lake.png --env_blur 10 ``` + +```bash +# Wywołanie algorytmu photon mapping ze specyfikacją liczby fotonów i maksymalnej głębokości +python main.py --algorithm photon_mapping --max_depth --num_photons 1000 +``` diff --git a/code/photonmapping/main.py b/code/photonmapping/main.py deleted file mode 100644 index 9aec82d6..00000000 --- a/code/photonmapping/main.py +++ /dev/null @@ -1,221 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt - -# Define basic vector operations -class Vector3: - def __init__(self, x, y, z): - self.x = x - self.y = y - self.z = z - - def __add__(self, other): - return Vector3(self.x+other.x, self.y+other.y, self.z+other.z) - - def __sub__(self, other): - return Vector3(self.x-other.x, self.y-other.y, self.z-other.z) - - def __mul__(self, scalar): - return Vector3(self.x*scalar, self.y*scalar, self.z*scalar) - - def dot(self, other): - return self.x*other.x + self.y*other.y + self.z*other.z - - def norm(self): - return np.sqrt(self.dot(self)) - - def normalize(self): - n = self.norm() - return Vector3(self.x/n, self.y/n, self.z/n) - -# Define the photon -class Photon: - def __init__(self, position, direction, power): - self.position = position - self.direction = direction - self.power = power - -# Define a simple sphere -class Sphere: - def __init__(self, center, radius, color): - self.center = center - self.radius = radius - self.color = color - - def intersect(self, ray_origin, ray_direction): - # Solve quadratic equation for intersection - oc = ray_origin - self.center - a = ray_direction.dot(ray_direction) - b = 2.0 * oc.dot(ray_direction) - c = oc.dot(oc) - self.radius*self.radius - discriminant = b*b - 4*a*c - if discriminant < 0: - return None # No intersection - else: - t = (-b - np.sqrt(discriminant)) / (2.0*a) - if t < 0: - t = (-b + np.sqrt(discriminant)) / (2.0*a) - if t < 0: - return None - hit_point = ray_origin + ray_direction * t - normal = (hit_point - self.center).normalize() - return (t, hit_point, normal) - -# Define a simple plane -class Plane: - def __init__(self, point, normal, color): - self.point = point - self.normal = normal.normalize() - self.color = color - - def intersect(self, ray_origin, ray_direction): - denom = self.normal.dot(ray_direction) - if abs(denom) > 1e-6: - t = (self.point - ray_origin).dot(self.normal) / denom - if t >= 0: - hit_point = ray_origin + ray_direction * t - return (t, hit_point, self.normal) - return None - -# Scene setup -sphere = Sphere(Vector3(0, 0, -5), 1.0, np.array([1, 0, 0])) # Red sphere -plane = Plane(Vector3(0, -1, 0), Vector3(0, 1, 0), np.array([0.5, 0.5, 0.5])) # Gray plane - -objects = [sphere, plane] - -# Light source -light_position = Vector3(-5, 5, -5) -light_power = np.array([1, 1, 1]) * 1000 # Intense white light - -# Photon map -photon_map = [] - -# Parameters -num_photons = 10000 # Number of photons to emit -max_depth = 5 # Maximum number of bounces -gather_radius = 0.5 # Radius for radiance estimation - -def emit_photons(): - for _ in range(num_photons): - # Emit photons in random directions from the light source - direction = random_unit_vector() - power = light_power / num_photons - photon = Photon(light_position, direction, power) - trace_photon(photon, 0) - -def trace_photon(photon, depth): - if depth > max_depth: - return - closest_t = np.inf - hit_object = None - hit_info = None - # Find the nearest intersection - for obj in objects: - result = obj.intersect(photon.position, photon.direction) - if result: - t, hit_point, normal = result - if t < closest_t: - closest_t = t - hit_object = obj - hit_info = (hit_point, normal) - if hit_object: - hit_point, normal = hit_info - photon_map.append((hit_point, photon.power)) - # Diffuse reflection - new_direction = random_hemisphere_direction(normal) - photon.position = hit_point - photon.direction = new_direction - # Absorb some power - photon.power = photon.power * 0.8 # Simple absorption - trace_photon(photon, depth+1) - -def random_unit_vector(): - theta = np.random.uniform(0, 2*np.pi) - z = np.random.uniform(-1, 1) - r = np.sqrt(1 - z*z) - return Vector3(r * np.cos(theta), r * np.sin(theta), z) - -def random_hemisphere_direction(normal): - dir = random_unit_vector() - if dir.dot(normal) < 0: - dir = Vector3(-dir.x, -dir.y, -dir.z) - return dir - -def render_image(width, height): - aspect_ratio = width / height - fov = np.pi / 3 # 60 degrees field of view - image = np.zeros((height, width, 3)) - for y in range(height): - for x in range(width): - # Convert pixel coordinate to camera ray - px = (2 * (x + 0.5) / width - 1) * np.tan(fov / 2) * aspect_ratio - py = (1 - 2 * (y + 0.5) / height) * np.tan(fov / 2) - ray_origin = Vector3(0, 0, 0) - ray_direction = Vector3(px, py, -1).normalize() - color = trace_ray(ray_origin, ray_direction) - image[y, x, :] = np.clip(color, 0, 1) - return image - -def trace_ray(ray_origin, ray_direction): - closest_t = np.inf - hit_object = None - hit_info = None - # Find the nearest intersection - for obj in objects: - result = obj.intersect(ray_origin, ray_direction) - if result: - t, hit_point, normal = result - if t < closest_t: - closest_t = t - hit_object = obj - hit_info = (hit_point, normal, obj.color) - if hit_object: - hit_point, normal, color = hit_info - direct_light = compute_direct_light(hit_point, normal) - indirect_light = estimate_radiance(hit_point, normal) - return color * (direct_light + indirect_light) - else: - return np.array([0, 0, 0]) # Background color - -def compute_direct_light(point, normal): - # Simple Lambertian reflection from light source - direction_to_light = (light_position - point).normalize() - # Shadow ray - shadow_origin = point + normal * 1e-5 - shadow_ray = direction_to_light - in_shadow = False - for obj in objects: - result = obj.intersect(shadow_origin, shadow_ray) - if result: - in_shadow = True - break - if in_shadow: - return np.array([0, 0, 0]) - else: - intensity = max(0, normal.dot(direction_to_light)) - return intensity * light_power / (4 * np.pi * (light_position - point).norm()**2) - -def estimate_radiance(point, normal): - # Gather photons within the gather_radius - accumulated_power = np.array([0.0, 0.0, 0.0]) - for photon_pos, photon_power in photon_map: - distance = (photon_pos - point).norm() - if distance < gather_radius: - weight = max(0, normal.dot((photon_pos - point).normalize())) - accumulated_power += photon_power * weight - area = np.pi * gather_radius ** 2 - return accumulated_power / (area * num_photons) - -# Main execution -if __name__ == '__main__': - print("Emitting photons...") - emit_photons() - - print("Rendering image...") - width = 200 - height = 100 - image = render_image(width, height) - - # Display the image - plt.imshow(image) - plt.axis('off') - plt.show() diff --git a/config.ini b/config.ini index a688a058..987bfe7b 100644 --- a/config.ini +++ b/config.ini @@ -7,8 +7,10 @@ resolution = 400x300 output = output.png [ray_tracing] -max_depth = 5 ; Params for ray tracing +max_depth = 5 samples_per_pixel = 6 [photon_mapping] -photon_count = 100000 ; Params for photon mapping \ No newline at end of file +num_photons = 10000 +max_depth = 5 +gather_radius = 0.5 \ No newline at end of file diff --git a/main.py b/main.py index 9e68b148..53d8a8db 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,10 @@ import argparse -from configparser import ConfigParser from rendering import ray_trace from utils import load_config, parse_resolution import importlib import os -# from scenes.cornell_box import * +import matplotlib.pyplot as plt +from photon_mapping import render_photon_mapping def main(): # default config @@ -22,6 +22,12 @@ def main(): parser.add_argument("--output", type=str, default=config.get('DEFAULT', 'output'), help="Output file name.") parser.add_argument('--num_spheres', type=int, default=3, help='Number of spheres in the scene for Ray Tracing 0') + parser.add_argument('--num_photons', type=int, default=config.getint('photon_mapping', 'num_photons'), help='Number of photons for photon mapping') + parser.add_argument('--max_depth', type=int, default=config.getint('photon_mapping', 'max_depth'), + help='Maximum depth for photon tracing') + parser.add_argument('--gather_radius', type=float, default=config.getfloat('photon_mapping', 'gather_radius'), + help='Radius for radiance estimation in photon mapping') + args = parser.parse_args() @@ -53,6 +59,15 @@ def main(): img.save(output_path) print(f"Image saved to {output_path}") img.show() + elif args.algorithm == "photon_mapping": + print("Starting photon mapping...") + image = render_photon_mapping(width, height, args.num_photons, args.max_depth, args.gather_radius) + plt.imshow(image) + plt.axis('off') + output_path = os.path.join("outputs", args.output) + plt.savefig(output_path) + print(f"Image saved to {output_path}") + plt.show() else: print(f"Unknown algorithm: {args.algorithm}") return diff --git a/outputs/output.png b/outputs/output.png index a23f6b31..dddfe4ce 100644 Binary files a/outputs/output.png and b/outputs/output.png differ diff --git a/photon_mapping.py b/photon_mapping.py new file mode 100644 index 00000000..befe273c --- /dev/null +++ b/photon_mapping.py @@ -0,0 +1,191 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Define basic vector operations +class Vector3: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __add__(self, other): + return Vector3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other): + return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __mul__(self, scalar): + return Vector3(self.x * scalar, self.y * scalar, self.z * scalar) + + def dot(self, other): + return self.x * other.x + self.y * other.y + self.z * other.z + + def norm(self): + return np.sqrt(self.dot(self)) + + def normalize(self): + n = self.norm() + return Vector3(self.x / n, self.y / n, self.z / n) + + +class Photon: + def __init__(self, position, direction, power): + self.position = position + self.direction = direction + self.power = power + + +class Sphere: + def __init__(self, center, radius, color): + self.center = center + self.radius = radius + self.color = color + + def intersect(self, ray_origin, ray_direction): + oc = ray_origin - self.center + a = ray_direction.dot(ray_direction) + b = 2.0 * oc.dot(ray_direction) + c = oc.dot(oc) - self.radius * self.radius + discriminant = b * b - 4 * a * c + if discriminant < 0: + return None + else: + t = (-b - np.sqrt(discriminant)) / (2.0 * a) + if t < 0: + t = (-b + np.sqrt(discriminant)) / (2.0 * a) + if t < 0: + return None + hit_point = ray_origin + ray_direction * t + normal = (hit_point - self.center).normalize() + return (t, hit_point, normal) + + +class Plane: + def __init__(self, point, normal, color): + self.point = point + self.normal = normal.normalize() + self.color = color + + def intersect(self, ray_origin, ray_direction): + denom = self.normal.dot(ray_direction) + if abs(denom) > 1e-6: + t = (self.point - ray_origin).dot(self.normal) / denom + if t >= 0: + hit_point = ray_origin + ray_direction * t + return (t, hit_point, self.normal) + return None + + +def render_photon_mapping(width, height, num_photons, max_depth, gather_radius): + # Photon mapping logic + photon_map = [] + sphere = Sphere(Vector3(0, 0, -5), 1.0, np.array([1, 0, 0])) # Red sphere + plane = Plane(Vector3(0, -1, 0), Vector3(0, 1, 0), np.array([0.5, 0.5, 0.5])) # Gray plane + objects = [sphere, plane] + + light_position = Vector3(-5, 5, -5) + light_power = np.array([1, 1, 1]) * 1000 + + def emit_photons(): + for _ in range(num_photons): + direction = random_unit_vector() + power = light_power / num_photons + photon = Photon(light_position, direction, power) + trace_photon(photon, 0) + + def trace_photon(photon, depth): + if depth > max_depth: + return + closest_t = np.inf + hit_object = None + hit_info = None + for obj in objects: + result = obj.intersect(photon.position, photon.direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal) + if hit_object: + hit_point, normal = hit_info + photon_map.append((hit_point, photon.power)) + new_direction = random_hemisphere_direction(normal) + photon.position = hit_point + photon.direction = new_direction + photon.power = photon.power * 0.8 + trace_photon(photon, depth + 1) + + def random_unit_vector(): + theta = np.random.uniform(0, 2 * np.pi) + z = np.random.uniform(-1, 1) + r = np.sqrt(1 - z * z) + return Vector3(r * np.cos(theta), r * np.sin(theta), z) + + def random_hemisphere_direction(normal): + dir = random_unit_vector() + if dir.dot(normal) < 0: + dir = Vector3(-dir.x, -dir.y, -dir.z) + return dir + + def trace_ray(ray_origin, ray_direction): + closest_t = np.inf + hit_object = None + hit_info = None + for obj in objects: + result = obj.intersect(ray_origin, ray_direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal, obj.color) + if hit_object: + hit_point, normal, color = hit_info + direct_light = compute_direct_light(hit_point, normal) + indirect_light = estimate_radiance(hit_point, normal) + return color * (direct_light + indirect_light) + else: + return np.array([0, 0, 0]) + + def compute_direct_light(point, normal): + direction_to_light = (light_position - point).normalize() + shadow_origin = point + normal * 1e-5 + shadow_ray = direction_to_light + in_shadow = False + for obj in objects: + result = obj.intersect(shadow_origin, shadow_ray) + if result: + in_shadow = True + break + if in_shadow: + return np.array([0, 0, 0]) + else: + intensity = max(0, normal.dot(direction_to_light)) + return intensity * light_power / (4 * np.pi * (light_position - point).norm() ** 2) + + def estimate_radiance(point, normal): + accumulated_power = np.array([0.0, 0.0, 0.0]) + for photon_pos, photon_power in photon_map: + distance = (photon_pos - point).norm() + if distance < gather_radius: + weight = max(0, normal.dot((photon_pos - point).normalize())) + accumulated_power += photon_power * weight + area = np.pi * gather_radius ** 2 + return accumulated_power / area + + emit_photons() + + aspect_ratio = width / height + fov = np.pi / 3 + image = np.zeros((height, width, 3)) + for y in range(height): + for x in range(width): + px = (2 * (x + 0.5) / width - 1) * np.tan(fov / 2) * aspect_ratio + py = (1 - 2 * (y + 0.5) / height) * np.tan(fov / 2) + ray_origin = Vector3(0, 0, 0) + ray_direction = Vector3(px, py, -1).normalize() + color = trace_ray(ray_origin, ray_direction) + image[y, x, :] = np.clip(color, 0, 1) + + return image