def scatter(self, incident_ray: Ray, record: "HitRecord") -> ReflectionRecord: reflected: Vec3 = reflect(incident_ray.direction.unit_vector(), record.normal) scattered: Ray = Ray( record.p, reflected + self.fuzz * random_unit_sphere_point()) return ReflectionRecord(self.albedo, scattered)
def test_ray_position(): r = Ray(Point(2, 3, 4), Vector(1, 0, 0)) assert r.position(0) == Point(2, 3, 4) assert r.position(1) == Point(3, 3, 4) assert r.position(-1) == Point(1, 3, 4) assert r.position(2.5) == Point(4.5, 3, 4)
def test_ray_init(): origin = Point(1, 2, 3) direct = Vector(4, 5, 6) r = Ray(origin, direct) assert r.origin == origin assert r.direction == direct
def test_ray_intersect_sphere_tangent(): r = Ray(Point(0, 1, -5), Vector(0, 0, 1)) s = Sphere() x = r.intersects(s) assert len(x) == 2 assert x[0].t == 5 assert x[1].t == 5
def test_sphere_intersection_scaled(): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() s.set_transform(scaling(2, 2, 2)) x = r.intersects(s) assert len(x) == 2 assert x[0].t == 3 assert x[1].t == 7
def test_ray_intersects_sphere_object(): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() x = r.intersects(s) assert len(x) == 2 assert x[0].object == s assert x[1].object == s
def scatter(self, incident_ray: Ray, record: "HitRecord") -> ReflectionRecord: # Lots of Physics I don't understand :\ target: Vec3 = record.p + record.normal + random_unit_sphere_point() scattered: Ray = Ray(record.p, target - record.p) attenuation: Vec3 = self.albedo reflecord: ReflectionRecord = ReflectionRecord(attenuation, scattered) return reflecord
def test_ray_intersect_from_middle(): r = Ray(Point(0, 0, 0), Vector(0, 0, 1)) s = Sphere() x = r.intersects(s) assert len(x) == 2 assert x[0].t == -1 assert x[1].t == 1
def test_ray_intersect_from_behind(): r = Ray(Point(0, 0, 5), Vector(0, 0, 1)) s = Sphere() x = r.intersects(s) assert len(x) == 2 assert x[0].t == -6 assert x[1].t == -4
def get_ray(self, s: float, t: float) -> Ray: random_point_in_disc: Vec3 = random_in_unit_disk() * self.lens_radius offset: Vec3 = ( self.__u * random_point_in_disc.x + self.__v * random_point_in_disc.y ) return Ray( self.origin + offset, self.lower_left_corner + (self.h_movement * s) + (self.v_movement * t) - self.origin - offset )
def scatter(self, incident_ray: Ray, record: "HitRecord") -> ReflectionRecord: reflected: Vec3 = reflect(incident_ray.direction, record.normal) attenuation: Vec3 = Vec3(1, 1, 1) # These are just placeholders; the following conditional block is their # actual "initial values". outward_normal: Vec3 = Vec3(1, 1, 1) nint: float = 0 cosine: float = 0 if incident_ray.direction.dot(record.normal) > 0: outward_normal = -1 * record.normal nint = self.refractive_index cosine = (self.refractive_index * incident_ray.direction.dot(record.normal) / incident_ray.direction.length()) else: outward_normal = record.normal nint = 1 / self.refractive_index cosine = -(incident_ray.direction.dot(record.normal) / incident_ray.direction.length()) # All this convoluted mix-up with _refracted and refracted is just # for type consistency. refracted: Vec3 = Vec3(0, 0, 0) _refracted: Optional[Vec3] = refract(incident_ray.direction, outward_normal, nint) reflection_probability: float = 1 if _refracted is not None: reflection_probability = self.__schlick_approximation(cosine) refracted = _refracted if reflection_probability == 1: return ReflectionRecord(attenuation, Ray(record.p, reflected)) else: return ReflectionRecord(attenuation, Ray(record.p, refracted))
def color(ray: Ray, world: HittableList) -> Vec3: # Some reflected rays hit not at zero but at some near-zero value due to # floating point shennanigans. So we try to compensate for that. hit_attempt: Optional[HitRecord] = world.hit(ray, 0.001, sys.float_info.max) if hit_attempt is not None: target: Vec3 = hit_attempt.p + hit_attempt.normal + random_unit_sphere_point( ) # FIXME mmm recursion # reflector_rate * reflected_color # So in this case, the matterial is a 50% reflector. return 0.5 * color(Ray(hit_attempt.p, target - hit_attempt.p), world) else: unit_direction: Vec3 = ray.direction.unit_vector() t: float = 0.5 * (unit_direction.y + 1) return ((1.0 - t) * UNIT_VEC3) + (t * Vec3(0.5, 0.7, 1.0))
def test_scale(): r = Ray(Point(1, 2, 3), Vector(0, 1, 0)) m = scaling(2, 3, 4) r2 = r.transform(m) assert r2.origin == Point(2, 6, 12) assert r2.direction == Vector(0, 3, 0)
def test_translate(): r = Ray(Point(1, 2, 3), Vector(0, 1, 0)) m = translation(3, 4, 5) r2 = r.transform(m) assert r2.origin == Point(4, 6, 8) assert r2.direction == Vector(0, 1, 0)
vertical = Vec3(data=(0., VIEWPORT_HEIGHT, 0.)) lower_left_corner = origin - horizontal / 2 - vertical / 2 x = np.tile( np.linspace(lower_left_corner.x(), lower_left_corner.x() + viewport_width, IMAGE_WIDTH), image_height) y = np.repeat( np.linspace(lower_left_corner.y(), lower_left_corner.y() + VIEWPORT_HEIGHT, image_height), IMAGE_WIDTH) colors = [] for i in range(NUM_SAMPLES): x_fidget = np.random.rand( IMAGE_WIDTH * image_height) / (IMAGE_WIDTH - 1) y_fidget = np.random.rand( IMAGE_WIDTH * image_height) / (image_height - 1) uv = Vec3(data=(x + x_fidget, y + y_fidget, -focal_length)) r = Ray(origin, uv - origin) colors.append(ray_color(r, world)) for y in range(image_height): for x in range(IMAGE_WIDTH): u = x / IMAGE_WIDTH v = (image_height - y) / image_height write_color(data, y, x, colors, NUM_SAMPLES) matplotlib.image.imsave('out.png', data) print(f"Finished in {time.time() - start} seconds")
def test_sphere_intersection_translate(): r = Ray(Point(0, 0, -5), Vector(0, 0, 1)) s = Sphere() s.set_transform(translation(5, 0, 0)) x = r.intersects(s) assert len(x) == 0
from src.canvas import Canvas from src.color import Color from src.primitives import Sphere from src.ray import Ray from src.transformations import scaling, rotation_z from src.tupl import Point s = Sphere() t = rotation_z(pi / 4) @ scaling(0.5, 1, 1) s.set_transform(t) r_origin = Point(0, 0, -5) wall_z = 10 wall_size = 7 N = 100 c = Canvas(N, N) pixel_size = wall_size / N half = wall_size / 2 red = Color(255, 0, 0) for y in range(c.height): world_y = half - pixel_size * y for x in range(c.width): world_x = -half + pixel_size * x position = Point(world_x, world_y, wall_z) r = Ray(r_origin, (position - r_origin).normalize()) X = r.intersects(s) if X.hit is not None: c.write_pixel(x, y, red) c.to_ppm('circle.ppm')
def get_ray(self, u: float, v: float) -> Ray: return Ray( self.origin, self.lower_left_corner + (self.h_movement * u) + (self.v_movement * v) - self.origin )
width = 400 height = 200 ppm: PPM = PPM(width, height) lower_left_corner: Vec3 = Vec3(-2, -1, -1) h_movement: Vec3 = Vec3(4, 0, 0) v_movement: Vec3 = Vec3(0, 2, 0) origin: Vec3 = Vec3(0, 0, 0) for j in range(height - 1, -1, -1): for i in range(width): # Get the ratio of how far are we from the "edges". u = i / width v = j / height # And use those ratios to "move" the ray away from the origin. Note: # - (h_ * u) + (v_ * v) is scaled movement # - the movement scales differently depending on dimension: # horizontal movement is increasing towards the vector (4, 0, 0) # while vertical movement is decreasing from the vector (0, 2, 0) r: Ray = Ray( origin, lower_left_corner + (h_movement * u) + (v_movement * v)) _color: Vec3 = color(r) _color *= 255.9 _color.map(int) # Note the translation for the row: we want the white part of the # gradient at the bottom so we do this. ppm.set_pixel((height - 1) - j, i, _color) ppm.write(_derive_ppm_filename())
def test_ray_intersect_sphere_no_intersection(): r = Ray(Point(0, 2, -5), Vector(0, 0, 1)) s = Sphere() x = r.intersects(s) assert len(x) == 0