Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
    def hit(self, ray: Ray, t_min: float, t_max: float) -> Optional[HitRecord]:
        origin_to_center: Vec3 = ray.origin - self.center
        # The following are just "components" of the quadratic formula, derived
        # from vectors and with some redundant 2's canceled out to begin with.
        a: float = ray.direction.dot(ray.direction)
        b: float = origin_to_center.dot(ray.direction)
        c: float = origin_to_center.dot(origin_to_center) - (self.radius**2)
        discriminant: float = (b**2) - (a * c)

        if discriminant > 0:
            neg_conjugate: float = (-b - math.sqrt(discriminant)) / a
            # The original C++ code, again, saves on stack space by re-using a
            # generic variable `temp`. But heh, I figured can't I skimp a bit on
            # the manual optimization and leave that up to the interpeter?
            pos_conjugate: float = (-b + math.sqrt(discriminant)) / a

            # Also this code was "refactored" from the original C++ to read more
            # Pythonic.
            chosen_conjugate = self.__decide_conjugate(t_min, t_max,
                                                       neg_conjugate,
                                                       pos_conjugate)
            if chosen_conjugate is not None:
                t = chosen_conjugate
                p = ray.point_at_parameter(t)
                normal = (p - self.center) / self.radius
                return HitRecord(t, p, normal, self.material)

        return None
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
def color(ray: Ray) -> Vec3:
    """
    Linear interpolation of color based on the y direction.

    As for hitting the sphere, note that the brightness of an object with
    respect to its light source is dependent on its normal vectors. The greater
    the angle between the normal and the light ray, the darker that spot is.
    Obvious implication: the sphere is brightest where the angle between the
    normal and the light ray is 0---that is, when the light ray is parallel to
    the normal.
    """
    t: float = hit_sphere(Vec3(0, 0, -1), 0.5, ray)
    if t > 0:
        normal: Vec3 = (ray.point_at_parameter(t) -
                        Vec3(0, 0, -1)).unit_vector()
        # For all I can tell, what this does is to "scale" the normal such that
        # (a) there are no negatives and (b) it will not all devolve to just 0,
        # which leaves us with just a black circle. Multiplying by .5 ensures
        # that the resulting sphere isn't too dark or too light.
        #
        # - Without the scaling factor (0.5), the image is just too bright.
        # - Without the increments to each component of the normal, the
        #   resulting image will tend to have negative values, which are invalid
        #   in the PPM spec to begin with, and just plain doesn't make sense.
        return 0.5 * Vec3(normal.x + 1, normal.y + 1, normal.z + 1)

    unit_direction: Vec3 = ray.direction.unit_vector()
    # Note that t's definition was pulled up. I guess he was just saving on some
    # stack space.
    t = 0.5 * (unit_direction.y + 1)
    return (UNIT_VEC3 * (1.0 - t)) + (Vec3(0.5, 0.7, 1.0) * t)
Ejemplo n.º 10
0
 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)
Ejemplo n.º 11
0
 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
Ejemplo n.º 12
0
 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
     )
Ejemplo n.º 13
0
    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))
Ejemplo n.º 14
0
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))
Ejemplo n.º 15
0
    def hit(self, r: Ray, t_min, t_max) -> HitRecord:
        oc = r.origin - self.center
        a = r.direction.squared_length()
        half_b = oc.dot(r.direction)
        c = oc.squared_length() - self.radius * self.radius
        discriminant = half_b * half_b - a * c
        root = (-half_b + np.sqrt(discriminant)) / a
        first_root = np.where(
            (discriminant > 0) & (root > t_min) & (root < t_max), root, t_max)
        root = (-half_b - np.sqrt(discriminant)) / a
        second_root = np.where(
            (discriminant > 0) & (root > t_min) & (root < t_max), root,
            first_root)

        p = vec_where(second_root < t_max, r.at(second_root),
                      Vec3(data=(np.Inf, np.Inf, np.Inf)))
        rec = HitRecord(t=second_root, p=p)
        rec.normal = vec_where(second_root < t_max,
                               (p - self.center) / self.radius,
                               Vec3(data=(np.Inf, np.Inf, np.Inf)))
        return rec
Ejemplo n.º 16
0
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
Ejemplo n.º 17
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)
Ejemplo n.º 18
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)
Ejemplo n.º 19
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")
Ejemplo n.º 20
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')
Ejemplo n.º 21
0
 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
     )
Ejemplo n.º 22
0
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
Ejemplo n.º 23
0
    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())
Ejemplo n.º 24
0
s = Sphere()

r_origin = Point(0, 0, -5)
wall_z = 10
wall_size = 7

light_position = Point(-10, 10, 10)
light_color = Color(1, 1, 1)
light = PointLight(light_position, light_color)

N = 200
c = Canvas(N, N)
pixel_size = wall_size / N
half = wall_size / 2

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:
            point = r.position(X.hit.t)
            normal = X.hit.object.normal_at(point)
            eye = -r.direction
            color = X.hit.object.material.lighting(light, point, eye, normal)
            c.write_pixel(x, y, color)
c.to_ppm('circle_shaded.ppm')