# A simple ray tracer # MIT license; Copyright (c) 2019 Damien P. George INF = 1e30 EPS = 1e-6 class Vec: def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z def __neg__(self): return Vec(-self.x, -self.y, -self.z) def __add__(self, rhs): return Vec(self.x + rhs.x, self.y + rhs.y, self.z + rhs.z) def __sub__(self, rhs): return Vec(self.x - rhs.x, self.y - rhs.y, self.z - rhs.z) def __mul__(self, rhs): return Vec(self.x * rhs, self.y * rhs, self.z * rhs) def length(self): return (self.x**2 + self.y**2 + self.z**2) ** 0.5 def normalise(self): l = self.length() return Vec(self.x / l, self.y / l, self.z / l) def dot(self, rhs): return self.x * rhs.x + self.y * rhs.y + self.z * rhs.z RGB = Vec class Ray: def __init__(self, p, d): self.p, self.d = p, d class View: def __init__(self, width, height, depth, pos, xdir, ydir, zdir): self.width = width self.height = height self.depth = depth self.pos = pos self.xdir = xdir self.ydir = ydir self.zdir = zdir def calc_dir(self, dx, dy): return (self.xdir * dx + self.ydir * dy + self.zdir * self.depth).normalise() class Light: def __init__(self, pos, colour, casts_shadows): self.pos = pos self.colour = colour self.casts_shadows = casts_shadows class Surface: def __init__(self, diffuse, specular, spec_idx, reflect, transp, colour): self.diffuse = diffuse self.specular = specular self.spec_idx = spec_idx self.reflect = reflect self.transp = transp self.colour = colour @staticmethod def dull(colour): return Surface(0.7, 0.0, 1, 0.0, 0.0, colour * 0.6) @staticmethod def shiny(colour): return Surface(0.2, 0.9, 32, 0.8, 0.0, colour * 0.3) @staticmethod def transparent(colour): return Surface(0.2, 0.9, 32, 0.0, 0.8, colour * 0.3) class Sphere: def __init__(self, surface, centre, radius): self.surface = surface self.centre = centre self.radsq = radius**2 def intersect(self, ray): v = self.centre - ray.p b = v.dot(ray.d) det = b**2 - v.dot(v) + self.radsq if det > 0: det **= 0.5 t1 = b - det if t1 > EPS: return t1 t2 = b + det if t2 > EPS: return t2 return INF def surface_at(self, v): return self.surface, (v - self.centre).normalise() class Plane: def __init__(self, surface, centre, normal): self.surface = surface self.normal = normal.normalise() self.cdotn = centre.dot(normal) def intersect(self, ray): ddotn = ray.d.dot(self.normal) if abs(ddotn) > EPS: t = (self.cdotn - ray.p.dot(self.normal)) / ddotn if t > 0: return t return INF def surface_at(self, p): return self.surface, self.normal class Scene: def __init__(self, ambient, light, objs): self.ambient = ambient self.light = light self.objs = objs def trace_scene(canvas, view, scene, max_depth): for v in range(canvas.height): y = (-v + 0.5 * (canvas.height - 1)) * view.height / canvas.height for u in range(canvas.width): x = (u - 0.5 * (canvas.width - 1)) * view.width / canvas.width ray = Ray(view.pos, view.calc_dir(x, y)) c = trace_ray(scene, ray, max_depth) canvas.put_pix(u, v, c) def trace_ray(scene, ray, depth): # Find closest intersecting object hit_t = INF hit_obj = None for obj in scene.objs: t = obj.intersect(ray) if t < hit_t: hit_t = t hit_obj = obj # Check if any objects hit if hit_obj is None: return RGB(0, 0, 0) # Compute location of ray intersection point = ray.p + ray.d * hit_t surf, surf_norm = hit_obj.surface_at(point) if ray.d.dot(surf_norm) > 0: surf_norm = -surf_norm # Compute reflected ray reflected = ray.d - surf_norm * (surf_norm.dot(ray.d) * 2) # Ambient light col = surf.colour * scene.ambient # Diffuse, specular and shadow from light source light_vec = scene.light.pos - point light_dist = light_vec.length() light_vec = light_vec.normalise() ndotl = surf_norm.dot(light_vec) ldotv = light_vec.dot(reflected) if ndotl > 0 or ldotv > 0: light_ray = Ray(point + light_vec * EPS, light_vec) light_col = trace_to_light(scene, light_ray, light_dist) if ndotl > 0: col += light_col * surf.diffuse * ndotl if ldotv > 0: col += light_col * surf.specular * ldotv**surf.spec_idx # Reflections if depth > 0 and surf.reflect > 0: col += trace_ray(scene, Ray(point + reflected * EPS, reflected), depth - 1) * surf.reflect # Transparency if depth > 0 and surf.transp > 0: col += trace_ray(scene, Ray(point + ray.d * EPS, ray.d), depth - 1) * surf.transp return col def trace_to_light(scene, ray, light_dist): col = scene.light.colour for obj in scene.objs: t = obj.intersect(ray) if t < light_dist: col *= obj.surface.transp return col class Canvas: def __init__(self, width, height): self.width = width self.height = height self.data = bytearray(3 * width * height) def put_pix(self, x, y, c): off = 3 * (y * self.width + x) self.data[off] = min(255, max(0, int(255 * c.x))) self.data[off + 1] = min(255, max(0, int(255 * c.y))) self.data[off + 2] = min(255, max(0, int(255 * c.z))) def write_ppm(self, filename): with open(filename, "wb") as f: f.write(bytes("P6 %d %d 255\n" % (self.width, self.height), "ascii")) f.write(self.data) def main(w, h, d): canvas = Canvas(w, h) view = View(32, 32, 64, Vec(0, 0, 50), Vec(1, 0, 0), Vec(0, 1, 0), Vec(0, 0, -1)) scene = Scene( 0.5, Light(Vec(0, 8, 0), RGB(1, 1, 1), True), [ Plane(Surface.dull(RGB(1, 0, 0)), Vec(-10, 0, 0), Vec(1, 0, 0)), Plane(Surface.dull(RGB(0, 1, 0)), Vec(10, 0, 0), Vec(-1, 0, 0)), Plane(Surface.dull(RGB(1, 1, 1)), Vec(0, 0, -10), Vec(0, 0, 1)), Plane(Surface.dull(RGB(1, 1, 1)), Vec(0, -10, 0), Vec(0, 1, 0)), Plane(Surface.dull(RGB(1, 1, 1)), Vec(0, 10, 0), Vec(0, -1, 0)), Sphere(Surface.shiny(RGB(1, 1, 1)), Vec(-5, -4, 3), 4), Sphere(Surface.dull(RGB(0, 0, 1)), Vec(4, -5, 0), 4), Sphere(Surface.transparent(RGB(0.2, 0.2, 0.2)), Vec(6, -1, 8), 4), ], ) trace_scene(canvas, view, scene, d) return canvas # For testing # main(256, 256, 4).write_ppm('rt.ppm') ########################################################################### # Benchmark interface bm_params = { (100, 100): (5, 5, 2), (1000, 100): (18, 18, 3), (5000, 100): (40, 40, 3), } def bm_setup(params): return lambda: main(*params), lambda: (params[0] * params[1] * params[2], None)