def test_one_plane(): scene_str = """ surfaces: s1: color: [0,0,253] objects: ground: type: plane point: [0, 0, 0] normal: [0, 1, 0] surface: s1 lights: {} camera: position: [0, 0, 0] direction: [1,-0.45,0] up: [1,1,0] """ scene, _ = parse_scene(scene_str) assert len(scene.objects) == 1 assert isinstance(scene.objects[0], Plane) assert scene.objects[0].point == Vector3(0, 0, 0) assert scene.objects[0].normal == Vector3(0, 1, 0) assert scene.objects[0].surface.color == Vector3(0, 0, 253)
def test_4_spheres(): surface = Surface(color=RED) s1 = Sphere(position=(10, -4, -4), radius=5, surface=surface) s2 = Sphere(position=(10, -4, 4), radius=5, surface=surface) s3 = Sphere(position=(10, 4, -4), radius=5, surface=surface) s4 = Sphere(position=(10, 4, 4), radius=5, surface=surface) scene = Scene(objects=[s1, s2, s3, s4], background=BLACK) # Center ray, miss all spheres ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, 0, 0)) d, obtained = scene.find_intersect(ray) assert obtained is None ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, 0.2, 0.2)) d, intersected = scene.find_intersect(ray) assert intersected == s4 assert d == approx(6.967, rel=1e-3) ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, -0.2, 0.2)) d, intersected = scene.find_intersect(ray) assert intersected == s2 assert d == approx(6.967, rel=1e-3) ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, -0.2, -0.2)) d, intersected = scene.find_intersect(ray) assert intersected == s1 assert d == approx(6.967, rel=1e-3) ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, 0.2, -0.2)) d, intersected = scene.find_intersect(ray) assert intersected == s3 assert d == approx(6.967, rel=1e-3)
def test_scalar_subtraction(): v1 = Vector3(1, 6, -1) obtained = v1 - 1 assert obtained == Vector3(0, 5, -2) obtained = 1 - v1 assert obtained == Vector3(0, -5, 2)
def test_center_pixel_position(): camera = Camera( Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0), screen_distance=5 ) screen1 = Screen(40, 30) camera.set_screen(screen1) print(f" n :{camera.n} u : {camera.u} v: {camera.v}") pixel_0_0 = camera.pixel_pos(0, 0) assert pixel_0_0 == camera.screen_corner print(f"0,0: {pixel_0_0}") pixel_40_0 = camera.pixel_pos(30, 0) print(f"40,0: {pixel_40_0}") pixel_0_30 = camera.pixel_pos(0, 40) print(f"0,30: {pixel_0_30}") pixel_40_30 = camera.pixel_pos(30, 40) print(f"40,30: {pixel_40_30}") # check screen is centered assert pixel_0_0.y == approx(-pixel_40_30.y) assert pixel_0_0.z == approx(-pixel_40_30.z) pixel_center = camera.pixel_pos(15, 20) assert pixel_center.as_tuple() == approx(Vector3(5, 0, 0).as_tuple())
def test_compute_camera_origin(): camera = Camera((0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0)) assert camera.n == Vector3(-1, 0, 0) assert camera.v == Vector3(0, 1, 0) assert camera.u == Vector3(0, 0, 1)
def test_scalar_addition(): v1 = Vector3(1, 6, -1) obtained = v1 + 2 assert obtained == Vector3(3, 8, 1) obtained = 2 + v1 assert obtained == Vector3(3, 8, 1)
def test_element_wise_addition(): v1 = Vector3(1, 6, -1) v2 = Vector3(0, 1, 2) obtained = v1 + v2 assert obtained == Vector3(1, 7, 1) obtained = v2 + v1 assert obtained == Vector3(1, 7, 1)
def test_intersect_single_sphere(): surface = Surface(color=RED) s = Sphere(position=(10, 0, 0), radius=5, surface=surface) scene = Scene(objects=[s], background=BLACK) ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, 0, 0)) obtained = scene.find_intersect(ray) assert obtained == (5, s)
def __init__(self, ambient_light=None, background=None, objects=None, light_sources=None): self.objects = [] if objects is None else objects self.light_sources = [] if light_sources is None else light_sources self.ambient_light = (Vector3(0.6, 0.6, 0.6) if ambient_light is None else Vector3(*ambient_light)) self.background = (Vector3(0, 0, 0) if background is None else Vector3( *background))
def test_cast_on_single_sphere(): surface = Surface(color=RED) s = Sphere(position=(10, 0, 0), radius=5, surface=surface) scene = Scene(objects=[s], background=BLACK) ray = Ray(origin=Vector3(0, 0, 0), direction=Vector3(1, 0, 0)) obtained = scene.cast_ray(ray) assert obtained.x != 0 assert obtained.y == 0 assert obtained.z == 0
def test_cast_miss_single_sphere(): surface = Surface(color=RED) s = Sphere(position=(10, 0, 0), radius=5, surface=surface) scene = Scene(objects=[s], background=BLACK) # Ray is above the sphere and should miss it ray = Ray(origin=Vector3(0, 6, 0), direction=Vector3(1, 0, 0)) obtained = scene.cast_ray(ray) assert obtained.x == 0 assert obtained.y == 0 assert obtained.z == 0
def __init__( self, diffuse=True, color=None, ka=None, kd=None, ks=None, alpha: int = 16, mirror_reflection=None, kr: float = None, ): """ Parameters ---------- diffuse: Boolean If True, the surface is shaded using the Phong model color: Vector3 Base color of the surface, given as RGB (0-255) ka: Vector3 Ambient reflection light coefficient (Phong model) kd: Vector3 Diffuse reflection coefficient (Phong model) ks: Vector3 Specular reflection coefficient (Phong model) alpha: int Shininess for specular reflexion (Phong model), large alpha produces small specular highlights , i.e. mirror like (=64) mirror_reflection: Vector3 coefficient for reflection, used when `mirror` is `True`. One [0-1] coefficient must be given for each RGB component. If given, the surface acts as a mirror and reflects light. kr: float Refractive index, used for computing refraction using Snell's Law. We assume the objects are placed in air, which has kr = 1. If given, the surface is transparent and lighting is made of refracted and reflected light, according to kr. """ self.diffuse = diffuse # Surface properties for Phong reflection model self.color = Vector3(*color) if color is not None else Vector3(0, 0, 0) self.ka = Vector3(*ka) if ka is not None else Vector3(0.9, 0.9, 0.9) self.kd = Vector3(*kd) if kd is not None else Vector3(0.8, 0.8, 0.8) self.ks = Vector3(*ks) if ks is not None else Vector3(1.2, 1.2, 1.2) self.alpha = alpha self.mirror_reflection = (Vector3( *mirror_reflection) if mirror_reflection is not None else None) self.kr = kr
def phong(self, point, normal, ray, scene): # ambient light ambient_coef = self.ka * scene.ambient_light # For each light source, diffuse and specular reflexion lights_coef = Vector3(0, 0, 0) for light in scene.light_sources: # Direction and distance to light light_segment = light.position - point light_dir = light_segment.normalize() light_power = light.power / (math.pi * light_segment.dot(light_segment)) # check if there is an object between the light source and the point outer_point = point + normal * NUDGE _, obj = scene.find_intersect(Ray(outer_point, light_dir), exclude=[self]) if obj: continue # Diffuse lightning: lights_coef += self.diffuse_lightning(normal, light_dir, light_power) # Specular reflexion lightning lights_coef += self.specular_reflexion(ray, normal, light_dir, light_power) return self.color * (ambient_coef + lights_coef)
def specular_reflexion(self, ray: Ray, normal: Vector3, light_dir: Vector3, light_power): spec_reflexion_dir = 2 * (light_dir.dot(normal)) * normal - light_dir view_dir = ray.direction * -1 spec_coef = view_dir.dot(spec_reflexion_dir) if spec_coef > 0: return (self.ks * math.pow(spec_coef, self.alpha)) * light_power return Vector3(0, 0, 0)
def __init__( self, position: Vector3, direction: Vector3, up: Vector3, field_of_view=math.pi * 0.4, screen_distance=10, ): self.position = Vector3(*position) self.direction = Vector3(*direction) # In which we are looking ! self.up = Vector3(*up) # viewing orientation self.field_of_view = field_of_view # angle, self.screen_distance = screen_distance # Compute basis vector at camera position: # need : position, coi and v_up self.n = -1 * self.direction.normalize() self.u = (self.up.cross(self.n)).normalize() self.v = self.n.cross(self.u) self.screen_3d_width, self.screen_3d_height = 0, 0
def test_intersect(): s = Sphere(Vector3(10, 0, 0), 5, surface=None) # Ray is straight to the sphere, we must have two intersection on the x axis: intersections = s.intersect(Ray(Vector3(0, 0, 0), Vector3(10, 0, 0))) assert intersections is not None assert intersections == 5 # Throw ray on y, while the sphere is on x assert not s.intersect(Ray(Vector3(0, 0, 0), Vector3(0, 1, 0))) # Throw ray on z, while the sphere is on x assert not s.intersect(Ray(Vector3(0, 0, 0), Vector3(0, 0, 1))) # More subtle assert s.intersect(Ray(Vector3(0, 0, 0), Vector3(1, 0.2, 0.1)))
def test_scene_one_light_source(): scene_str = """ surfaces: {} objects: {} lights: light1: position: [20,50,30] power: [1000, 1000, 1000] camera: position: [0, 0, 0] direction: [1,-0.45,0] up: [1,1,0] """ scene, _ = parse_scene(scene_str) assert len(scene.light_sources) == 1 assert isinstance(scene.light_sources[0], LightSource) assert scene.light_sources[0].position == Vector3(20, 50, 30) assert scene.light_sources[0].power == Vector3(1000, 1000, 1000)
def test_camera(): scene_str = """ surfaces: {} objects: {} lights: {} camera: position: [0, 0, 0] direction: [1,-0.45,0] up: [1,1,0] field_of_view: 2.3 screen_distance: 11 """ _, camera = parse_scene(scene_str) assert camera.position == Vector3(0, 0, 0) assert camera.direction == Vector3(1, -0.45, 0) assert camera.up == Vector3(1, 1, 0) assert camera.field_of_view == 2.3 assert camera.screen_distance == 11
def test_screen_size_and_position_do_not_depend_on_resolution(): camera = Camera( Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0), screen_distance=5 ) screen1 = Screen(40, 30) camera.set_screen(screen1) screen1_width = camera.screen_3d_width screen1_height = camera.screen_3d_height screen1_corner = camera.screen_corner screen2 = Screen(400, 300) camera.set_screen(screen2) screen2_width = camera.screen_3d_width screen2_height = camera.screen_3d_height screen2_corner = camera.screen_corner assert screen1_width == screen2_width assert screen1_height == screen2_height assert screen1_corner == screen2_corner print(f" {camera.screen_3d_width} {camera.screen_3d_height} {camera.screen_corner}")
def test_scene_one_sphere(): scene_str = """ surfaces: s1: color: [0,0,253] ka: [0.1, 0.2, 0.3] kd: [0.4, 0.5, 0.6] ks: [0.7, 0.8, 0.9] objects: sphere1: type: sphere position: [10, 10, 10] radius: 5 surface: s1 lights: {} camera: position: [0, 0, 0] direction: [1,-0.45,0] up: [1,1,0] """ scene, _ = parse_scene(scene_str) assert len(scene.objects) == 1 assert isinstance(scene.objects[0], Sphere) assert scene.objects[0].radius == 5 assert scene.objects[0].position == Vector3(10, 10, 10) assert scene.objects[0].surface.diffuse assert not scene.objects[0].surface.mirror_reflection assert not scene.objects[0].surface.kr assert scene.objects[0].surface.ka == Vector3(0.1, 0.2, 0.3) assert scene.objects[0].surface.kd == Vector3(0.4, 0.5, 0.6) assert scene.objects[0].surface.ks == Vector3(0.7, 0.8, 0.9) assert scene.objects[0].surface.color == Vector3(0, 0, 253)
def refraction_at(self, point, ray, normal, scene, depth): cos_out = normal.dot(ray.direction) if cos_out > 0: # getting out of the object: invert refraction coefficients n1 = self.kr n2 = 1 else: # Entering the object n1 = 1 n2 = self.kr cos_out = -cos_out n12 = n1 / n2 # Refraction + Reflexion # Assume we are moving from air (n= 1) to another material with nt # Ratio of reflected light, use Fresnel and Schilck approximation r0 = math.pow((n2 - 1) / (n2 + 1), 2) r = r0 + (1 - r0) * math.pow(1 - cos_out, 5) # Reflexion reflexion_dir = ray.direction - 2 * ( ray.direction.dot(normal)) * normal reflexion_ray = Ray(point + normal * NUDGE, reflexion_dir) reflexion_color = scene.cast_ray(reflexion_ray, depth - 1) # Refraction refraction_color = Vector3(255, 0, 0) dis = 1 - n12 * n12 * (1 - cos_out * cos_out) if dis > 0: # otherwise, no refraction, all is reflected refraction_dir = n12 * (ray.direction - normal * cos_out) - normal * math.sqrt(dis) refraction_ray = Ray(point - normal * NUDGE, refraction_dir) # Cast a refraction (aka transparency) ray: refraction_color = scene.cast_ray(refraction_ray, depth - 1) else: r = 1 color = r * reflexion_color + (1 - r) * refraction_color return color
def test_two_spheres(): scene = Scene() light2 = LightSource(Vector3(0, -20, -10)) surface = Surface(color=Vector3(100, 0, 0)) sphere1 = Sphere(Vector3(40, 4, 0), 3, surface) sphere2 = Sphere(Vector3(30, -4, 0), 3, surface) scene.objects.append(sphere1) scene.objects.append(sphere2) scene.light_sources.append(light2) camera = Camera(Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0)) screen = PngScreen("test_two_spheres.png", 400, 400) camera.set_screen(screen) camera.ray_for_pixel(50, 50) camera.take_picture(scene)
def color_at(self, point, ray, hit_normal, scene, depth): # Compute the color for a ray touching this surface, # using phong, reflection or transparent (reflexion + refraction + fresnel) color = Vector3(0, 0, 0) if self.diffuse: # Phong model for ambient, diffuse and specular reflexion light color += self.phong(point, hit_normal, ray, scene) if depth < 0: return color if self.mirror_reflection: # Reflexion only, mirror like color += self.mirror_reflection * self.reflexion_at( point, ray, hit_normal, scene, depth) return color elif self.kr: # Refraction color += self.refraction_at(point, ray, hit_normal, scene, depth) return color
def test_sphere_position(): scene = Scene() light2 = LightSource(Vector3(0, -20, -20)) surface = Surface(color=Vector3(100, 0, 0)) sphere1 = Sphere(Vector3(25, -3, -5), 3, surface) scene.objects.append(sphere1) scene.light_sources.append(light2) camera = Camera(Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(1, 1, 0), screen_distance=10) screen = PngScreen("test_sphere_position.png", 600, 400) camera.set_screen(screen) camera.ray_for_pixel(50, 50) camera.take_picture(scene)
def test_element_wise_division(): v1 = Vector3(1, 6, -1) v2 = Vector3(1, 2, -0.5) obtained = v1 / v2 assert obtained == Vector3(1, 3, 2)
def test_scalar_division(): v1 = Vector3(1, 6, -1) obtained = v1 / 2 assert obtained == Vector3(0.5, 3, -0.5)
def __init__(self, position, power=Vector3(1, 1, 1)): self.position = Vector3(*position) # Power of the light source, not restricted to [0-1] # The value depends on the scale of the scene you are using self.power = Vector3(*power)
def diffuse_lightning(self, normal: Vector3, light_dir: Vector3, light_power): dot_p = normal.dot(light_dir) if dot_p > 0: return (self.kd * dot_p) * light_power return Vector3(0, 0, 0)
def __init__(self, position: Vector3, radius: float, surface): super().__init__(surface) self.radius = radius self.position = Vector3(*position)
def __init__(self, point: Vector3, normal: Vector3, surface: Surface): super().__init__(surface) self.point = Vector3(*point) self.normal = Vector3(*normal).normalize()