├── modules ├── __init__.py ├── __pycache__ │ ├── color.cpython-38.pyc │ ├── image.cpython-38.pyc │ ├── light.cpython-38.pyc │ ├── ray.cpython-38.pyc │ ├── scene.cpython-38.pyc │ ├── engine.cpython-38.pyc │ ├── objects.cpython-38.pyc │ ├── vector.cpython-38.pyc │ ├── __init__.cpython-38.pyc │ └── material.cpython-38.pyc ├── light.py ├── ray.py ├── material.py ├── image.py ├── scene.py ├── color.py ├── objects.py ├── vector.py └── engine.py ├── output.png ├── README.md └── raytracer.py /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/output.png -------------------------------------------------------------------------------- /modules/__pycache__/color.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/color.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/image.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/image.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/light.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/light.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/ray.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/ray.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/scene.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/scene.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/engine.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/engine.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/objects.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/objects.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/vector.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/vector.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /modules/__pycache__/material.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silvaan/raytracer/HEAD/modules/__pycache__/material.cpython-38.pyc -------------------------------------------------------------------------------- /modules/light.py: -------------------------------------------------------------------------------- 1 | class Light: 2 | def __init__(self, position, color=(255, 255, 255), intensity=1.0): 3 | self.position = position 4 | self.color = color 5 | self.intensity = intensity -------------------------------------------------------------------------------- /modules/ray.py: -------------------------------------------------------------------------------- 1 | class Ray: 2 | def __init__(self, origin, direction): 3 | self.origin = origin 4 | self.direction = direction.normalize() 5 | 6 | def point(self, dist): 7 | return self.origin + dist*self.direction -------------------------------------------------------------------------------- /modules/material.py: -------------------------------------------------------------------------------- 1 | class Material: 2 | def __init__(self, color, ambient=0.05, diffuse=1.0, specular=1.0): 3 | self.color = color 4 | self.ambient = ambient 5 | self.diffuse = diffuse 6 | self.specular = specular -------------------------------------------------------------------------------- /modules/image.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Image: 4 | def __init__(self, width, height): 5 | self.width = width 6 | self.height = height 7 | self.pixels = np.zeros((height, width, 3)) 8 | 9 | def set_pixel(self, x, y, color): 10 | self.pixels[y, x] = color -------------------------------------------------------------------------------- /modules/scene.py: -------------------------------------------------------------------------------- 1 | from modules.image import Image 2 | 3 | class Scene: 4 | def __init__(self, camera, lights, objects, width, height): 5 | self.camera = camera 6 | self.lights = lights 7 | self.objects = objects 8 | self.width = width 9 | self.height = height 10 | self.image = Image(width, height) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Raytracer 2 | This is a simple implementation of a raytracer algorithm in Python for study purposes. Enjoy! 3 | 4 | ![Raytracer](output.png) 5 | 6 | ## References 7 | 8 | * https://gist.github.com/sevko/c3ed2430e96b89dd3177 9 | * https://github.com/arocks/puray 10 | * https://www.cs.unc.edu/~rademach/xroads-RT/RTarticle.html 11 | * https://www.scratchapixel.com/ -------------------------------------------------------------------------------- /raytracer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from modules.vector import Point, Vector 3 | from modules.color import Color 4 | from modules.light import Light 5 | from modules.material import Material 6 | from modules.objects import Sphere 7 | from modules.scene import Scene 8 | from modules.engine import Engine 9 | from PIL import Image 10 | 11 | camera = Point(0, 0, -1) 12 | 13 | lights = [ 14 | Light(Point(-5, -15, -2), Color('#ffffff'), 1.0), 15 | ] 16 | 17 | objects = [ 18 | Sphere(Point(0, 104, 35), 100, Material(Color('#ffffff'), specular=0.5)), 19 | Sphere(Point(-4.5, 1, 10), 3, Material(Color('#880000'), specular=0.6)), 20 | Sphere(Point(0, -4, 18), 5, Material(Color('#008800'), specular=0.6)), 21 | Sphere(Point(4.5, 1, 10), 3, Material(Color('#000088'), specular=0.6)), 22 | ] 23 | 24 | scene = Scene( 25 | camera = camera, 26 | lights = lights, 27 | objects = objects, 28 | width = 800, 29 | height = 600 30 | ) 31 | 32 | engine = Engine(scene, reflection_depth=5) 33 | engine.render() 34 | 35 | pixels = scene.image.pixels 36 | image = Image.fromarray(pixels.astype(np.uint8)) 37 | 38 | image.save('output.png') -------------------------------------------------------------------------------- /modules/color.py: -------------------------------------------------------------------------------- 1 | class Color: 2 | def __init__(self, value=(0, 0, 0)): 3 | if (isinstance(value, str)): 4 | self.r = int(value[1:3], 16) 5 | self.g = int(value[3:5], 16) 6 | self.b = int(value[5:7], 16) 7 | else: 8 | self.r = value[0] 9 | self.g = value[1] 10 | self.b = value[2] 11 | self.r = round(min(255, self.r)) 12 | self.g = round(min(255, self.g)) 13 | self.b = round(min(255, self.b)) 14 | self.value = (self.r, self.g, self.b) 15 | 16 | def __str__(self): 17 | return f'Color{self.value}' 18 | 19 | def __add__(self, other): 20 | return Color((self.r + other.r, self.g + other.g, self.b + other.b)) 21 | 22 | def __sub__(self, other): 23 | return Color((self.r - other.r, self.g - other.g, self.b - other.b)) 24 | 25 | def __mul__(self, other): 26 | return Color((self.r*other, self.g*other, self.b*other)) 27 | 28 | def __rmul__(self, other): 29 | return self.__mul__(other) 30 | 31 | def __truediv__(self, other): 32 | return Color((self.r/other, self.g/other, self.b/other)) 33 | 34 | def to_list(self): 35 | return list(self.value) -------------------------------------------------------------------------------- /modules/objects.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Plane: 4 | def __init__(self, origin, normal, material): 5 | self.origin = origin 6 | self.normal = normal.normalize() 7 | self.material = material 8 | 9 | def intersects(self, ray): 10 | denom = ray.direction * self.normal 11 | if denom > 1e-6: 12 | v = (self.origin - ray.origin) * self.normal 13 | dist = v / denom 14 | if dist > 0: 15 | return dist 16 | return None 17 | 18 | def normal_at(self, surf_point): 19 | return self.normal 20 | 21 | class Sphere: 22 | def __init__(self, origin, radius, material): 23 | self.origin = origin 24 | self.radius = radius 25 | self.material = material 26 | 27 | def intersects(self, ray): 28 | sphere_to_ray = ray.origin - self.origin 29 | b = 2 * (ray.direction * sphere_to_ray) 30 | c = sphere_to_ray * sphere_to_ray - self.radius * self.radius 31 | discriminant = b**2 - 4 * c 32 | 33 | if discriminant >= 0: 34 | dist = (-b - np.sqrt(discriminant)) / 2 35 | if dist > 0: 36 | return dist 37 | return None 38 | 39 | def normal_at(self, surf_point): 40 | return (surf_point - self.origin).normalize() -------------------------------------------------------------------------------- /modules/vector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Vector: 4 | def __init__(self, x=0, y=0, z=0): 5 | self.x = x 6 | self.y = y 7 | self.z = z 8 | self.norm = np.sqrt(self.x**2+self.y**2+self.z**2) 9 | 10 | def normalize(self): 11 | return self / self.norm 12 | 13 | def __str__(self): 14 | return f'Vector({self.x}, {self.y}, {self.z})' 15 | 16 | def __add__(self, other): 17 | return Vector(self.x + other.x, self.y + other.y, self.z + other.z) 18 | 19 | def __sub__(self, other): 20 | return Vector(self.x - other.x, self.y - other.y, self.z - other.z) 21 | 22 | def __mul__(self, other): 23 | if isinstance(other, Vector): 24 | return self.x*other.x + self.y*other.y + self.z*other.z 25 | else: 26 | return Vector(self.x*other, self.y*other, self.z*other) 27 | 28 | def __rmul__(self, other): 29 | return self.__mul__(other) 30 | 31 | def __truediv__(self, other): 32 | return Vector(self.x/other, self.y/other, self.z/other) 33 | 34 | def __pow__(self, exp): 35 | return self*self 36 | 37 | def reflect(self, other): 38 | other = other.normalize() 39 | return self - 2 * (self * other) * other 40 | 41 | Point = Vector -------------------------------------------------------------------------------- /modules/engine.py: -------------------------------------------------------------------------------- 1 | from modules.vector import Point 2 | from modules.ray import Ray 3 | from modules.color import Color 4 | 5 | class Engine: 6 | def __init__(self, scene, reflection_depth=3): 7 | self.scene = scene 8 | self.reflection_depth = reflection_depth 9 | 10 | def render(self): 11 | ratio = float(self.scene.width)/self.scene.height 12 | x0 = -1.0 13 | x1 = 1.0 14 | xstep = (x1-x0)/(self.scene.width-1) 15 | y0 = -1.0/ratio 16 | y1 = 1.0/ratio 17 | ystep = (y1-y0)/(self.scene.height-1) 18 | 19 | for j in range(self.scene.height): 20 | y = y0 + j*ystep 21 | for i in range(self.scene.width): 22 | x = x0 + i*xstep 23 | ray = Ray(self.scene.camera, Point(x, y) - self.scene.camera) 24 | self.scene.image.set_pixel(i, j, self.ray_trace(ray).to_list()) 25 | 26 | def ray_trace(self, ray, depth=0): 27 | color = Color() 28 | 29 | if depth >= self.reflection_depth: 30 | return color 31 | 32 | hit_dist, hit_obj = self.find_nearest(ray) 33 | if hit_obj is None or hit_dist is None: 34 | return color 35 | 36 | hit_pos = ray.point(hit_dist) 37 | normal = hit_obj.normal_at(hit_pos) 38 | material = hit_obj.material 39 | to_cam = self.scene.camera - hit_pos 40 | 41 | # Ambient 42 | color += material.color * material.ambient 43 | 44 | for light in self.scene.lights: 45 | to_light = Ray(hit_pos, light.position - hit_pos) 46 | hit_dist_, hit_obj_ = self.find_nearest(to_light) 47 | 48 | if hit_obj_ is None: 49 | # Difuse Shading 50 | color += (material.color + 0.3 * light.color * light.intensity) * material.diffuse * max(normal*to_light.direction, 0) 51 | 52 | # Reflection 53 | reflected_ray = Ray(hit_pos, ray.direction.reflect(normal).normalize()) 54 | color += self.ray_trace(reflected_ray, depth+1) * material.specular 55 | 56 | return color 57 | 58 | def find_nearest(self, ray): 59 | hit_dist = None 60 | hit_obj = None 61 | for obj in self.scene.objects: 62 | dist = obj.intersects(ray) 63 | if dist is not None and (hit_obj is None or dist < hit_dist): 64 | hit_dist = dist 65 | hit_obj = obj 66 | return hit_dist, hit_obj --------------------------------------------------------------------------------