Пример #1
0
def irandom_in_unit_disk() -> Vec3:
    point: Vec3 = Vec3(random.uniform(-1, 1), random.uniform(-1, 1), 0)
    
    while point.dot(point) >= 1:
        point = Vec3(random.uniform(-1, 1), random.uniform(-1, 1), 0)

    return point
Пример #2
0
    def test_dot(self):
        a = Vec3(2.0, 1.0, -1.0)
        b = Vec3(-3.0, 4.0, 1.0)

        adb = a.dot(b)
        expected = sum((a.x * b.x, a.y * b.y, a.z * b.z))
        self.assertEqual(expected, adb)
Пример #3
0
def random_unit_sphere_point() -> Vec3:
    """
    Pick a random point inside a unit sphere. To do this, we use a "rejection
    method":

    1. Pick a random point inside a cube with edges in the [-1, 1] range of all
    axes. Note that the unit sphere is inside this cube and this cube is "easy"
    to construct programmatically.
    2. Check whether the point is inside the unit sphere. If it is not, generate
    again. Do this until we get a point inside the unit sphere.

    The time complexity of this procedure is left as an exercise to the reader.
    """
    # Shirley uses the following formula to generate a random point in the
    # constraints specified:
    #   2 * rand_vector - unit_vector
    # Where rand_vector has components that range from [0, 1). While this is
    # understandable enough, I'm experimenting with random.uniform instead.
    rand_point: Vec3 = Vec3(random.uniform(-1, 1), random.uniform(-1, 1),
                            random.uniform(-1, 1))

    while rand_point.squared_length() >= 1:
        rand_point = Vec3(random.uniform(-1, 1), random.uniform(-1, 1),
                          random.uniform(-1, 1))

    return rand_point
Пример #4
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)
Пример #5
0
def random_in_unit_disk() -> Vec3:
    point: Vec3 = 2 * Vec3(random.random(), random.random(), 0) - Vec3(1, 1, 0)

    while point.dot(point) >= 1:
        point = 2 * Vec3(random.random(), random.random(), 0) - Vec3(1, 1, 0)

    return point
Пример #6
0
def color(ray: Ray, world: HittableList) -> Vec3:
    hit_attempt: Optional[HitRecord] = world.hit(ray, 0.0, sys.float_info.max)
    if hit_attempt is not None:
        return 0.5 * Vec3(hit_attempt.normal.x + 1, hit_attempt.normal.y + 1,
                          hit_attempt.normal.z + 1)
    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))
Пример #7
0
    def test_cross(self):
        a = Vec3(2.0, 1.0, -1.0)
        b = Vec3(-3.0, 4.0, 1.0)

        axb = a.cross(b)
        self.assertEqual(Vec3(5.0, 1.0, 11.0), axb)

        bxa = b.cross(a)
        self.assertEqual(Vec3(-5.0, -1.0, -11.0), bxa)
Пример #8
0
def random_scene(x_min: int, x_max: int, z_min: int,
                 z_max: int) -> List[Hittable]:
    # Start off the list with the "earth" sphere.
    world: List[Hittable] = [
        #Sphere(Vec3(0, -1000, 0), 1000, Lambertian(Vec3(0.5, 0.5, 0.5)))
    ]

    #for x in range(x_min, x_max):
    #    if len(world) >= 100:
    #        break
    #    for z in range(z_min, z_max):
    #        if len(world) >= 100:
    #            break
    #        material_decider: float = random.random()
    #        # TODO Clarify what the 0.9 constant here is for.
    #        center: Vec3 = Vec3(
    #            x + 0.9 * random.random(),
    #            0.2,
    #            z + 0.9 * random.random()
    #        )

    #        # TODO What is Vec3(4, 0.2, 0) for?
    #        if (center - Vec3(4, 0.2, 0)).length() > 0.9:
    #            if material_decider < 0.8:
    #                world.append(
    #                    Sphere(center, 0.2, Lambertian(Vec3(
    #                        random.random() * random.random(),
    #                        random.random() * random.random(),
    #                        random.random() * random.random()
    #                    )))
    #                )
    #            elif material_decider < 0.95:
    #                world.append(
    #                    Sphere(center, 0.2, Metal(
    #                        Vec3(
    #                            0.5 * (1 + random.random()),
    #                            0.5 * (1 + random.random()),
    #                            0.5 * (1 + random.random())
    #                        ),
    #                        0.5 * random.random()
    #                    ))
    #                )
    #            else:
    #                world.append(
    #                    Sphere(center, 0.2, Dielectric(1.5))
    #                )

    world.extend([
        Sphere(Vec3(0, 1, 0), 1.0, Dielectric(1.5)),
        Sphere(Vec3(-4, 1, 0), 1.0, Lambertian(Vec3(0.4, 0.2, 0.1))),
        Sphere(Vec3(4, 1, 0), 1.0, Metal(Vec3(0.7, 0.6, 0.5), 0.0))
    ])

    return world
Пример #9
0
def color(ray: Ray) -> Vec3:
    """
    Linear interpolation of color based on the y direction.
    """
    if hit_sphere(Vec3(0, 0, -1), 0.5, ray):
        return Vec3(1, 0, 0)

    unit_direction: Vec3 = ray.direction.unit_vector()
    # WOW lots of magic numbers!
    t: float = 0.5 * (unit_direction.y + 1)
    return (UNIT_VEC3 * (1.0 - t)) + (Vec3(0.5, 0.7, 1.0) * t)
Пример #10
0
 def __init__(
     self, camera_posn: Vec3, camera_aim: Vec3, up_vector: Vec3, vfov: float,
     aspect_ratio: float, aperture: float = 2, focus_dist: float = 1
 ):
     """
     Create a camera automatically positioned relative to the scene such that
     it has a certain vertical field-of-view (vfov, expressed in degrees)
     given the scene's aspect ration.
     """
     self.lens_radius: float = aperture / 2
     vfov_rad: float = vfov * math.pi / 180
     half_height: float = math.tan(vfov_rad / 2)
     half_width: float = aspect_ratio * half_height
     # The following are just some vectors to define axes. Don't be confused!
     self.__w = (camera_posn - camera_aim).unit_vector()
     self.__u = up_vector.cross(self.__w).unit_vector()
     self.__v = self.__w.cross(self.__u)
     super().__init__(
         camera_posn - focus_dist * (
             (half_width * self.__u) + (half_height * self.__v) + self.__w
         ),
         2 * half_width * focus_dist * self.__u,
         2 * half_height * focus_dist * self.__v,
         camera_posn
     )
Пример #11
0
def color(ray: Ray, world: HittableList, depth: int = 0) -> 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:
        reflection: ReflectionRecord = hit_attempt.material.scatter(
            ray, hit_attempt)
        if depth < 50 and reflection is not None:
            return reflection.attenuation * color(reflection.scattering, world,
                                                  depth + 1)
        else:
            return Vec3(0, 0, 0)
    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 color(ray: Ray, world: HittableList, depth: int) -> 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:
        scattering: ReflectionRecord = hit_attempt.material.scatter(ray, hit_attempt)
        # FIXME Is it really worthwhile to check if reflection is not None here?
        # All our Materials assume that a hit has been made, and therefore some
        # reflection should happen (unless it is Vanta).
        if depth < 50 and scattering is not None:
            # Compare this with the hard-coded reflection in 6_matterial.
            return scattering.attenuation * color(scattering.scattering, world, depth + 1)
        else:
            return Vec3(0, 0, 0)
    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))
Пример #13
0
class HitRecord:
    p: Vec3
    t: np.array
    normal: Vec3 = Vec3(data=(0., 0., 0.))
    front_face: np.array = None

    def set_face_normal(self, r: Ray, outward_normal):
        self.front_face = r.direction.dot(self.normal) < 0
        self.normal = vec_where(self.front_face, outward_normal,
                                outward_normal * -1.)
Пример #14
0
 def __init__(self,
              width: int,
              height: int,
              default_color: Optional[Vec3] = None):
     self.width = width
     self.height = height
     if default_color is None:
         default_color = Vec3(0, 0, 0)
     self.grid = [[default_color for _ in range(width)]
                  for __ in range(height)]
Пример #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
Пример #16
0
def refract(v: Vec3, n: Vec3, nint: float) -> Optional[Vec3]:
    """
    Return the refracting ray if the conditions are good for refraction. If it
    does not describe a refracting scenario, return None.
    """
    uv: Vec3 = v.unit_vector()
    dt: float = uv.dot(n)
    discriminant: float = 1 - (nint**2) * (1 - dt**2)

    if discriminant > 0:
        return (nint * (uv - n * dt) - n * math.sqrt(discriminant))

    return None
Пример #17
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))
Пример #18
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))
Пример #19
0
def reflect(v: Vec3, n: Vec3) -> Vec3:
    return v - 2 * v.dot(n) * n
Пример #20
0
from random import SystemRandom
from src.camera import Camera
from src.hittable import HitRecord, Hittable, HittableList
from src.ppm import PPM
from src.ray import Ray
from src.sphere import Sphere
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
from typing import List, Optional

import sys

UNIT_VEC3: Vec3 = Vec3(1.0, 1.0, 1.0)
random = SystemRandom()


def color(ray: Ray, world: HittableList) -> Vec3:
    hit_attempt: Optional[HitRecord] = world.hit(ray, 0.0, sys.float_info.max)
    if hit_attempt is not None:
        return 0.5 * Vec3(hit_attempt.normal.x + 1, hit_attempt.normal.y + 1,
                          hit_attempt.normal.z + 1)
    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))


if __name__ == "__main__":
    width = 400
    height = 200
    sampling_size = 200
Пример #21
0
    colors = (hits.normal.normalized() + Vec3(data=(1., 1., 1.))) * .5
    t = 0.5 * (r.direction.normalized().y() + 1.0)
    sky = Vec3(data=(1., 1., 1.)) * (1. - t) + Vec3(data=(0.5, 0.7, 1.0)) * t
    return vec_where(hits.t < MAX_VAL, colors, sky)


if __name__ == '__main__':
    start = time.time()
    # Image
    image_height = int(IMAGE_WIDTH / ASPECT_RATIO)
    out_shape = (image_height, IMAGE_WIDTH, 3)
    data = np.zeros(shape=out_shape)

    # World
    world = HittableList()
    world.add(Sphere(Vec3(data=(0., 0., -1.)), 0.5))
    world.add(Sphere(Vec3(data=(0., -100.5, -1.)), 100))

    # Camera
    viewport_width = ASPECT_RATIO * VIEWPORT_HEIGHT
    focal_length = 1
    origin = Vec3(data=(0., 0., 0.))
    horizontal = Vec3(data=(viewport_width, 0., 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(
Пример #22
0
from src.ppm import PPM
from src.ray import Ray
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
"""
We have an abstract, static camera at (0, 0, 0).
"""

UNIT_VEC3: Vec3 = Vec3(1.0, 1.0, 1.0)


def color(ray: Ray) -> Vec3:
    """
    Linear interpolation of color based on the y direction.
    """
    unit_direction: Vec3 = ray.direction.unit_vector()
    # WOW lots of magic numbers!
    t: float = 0.5 * (unit_direction.y + 1)
    return (UNIT_VEC3 * (1.0 - t)) + (Vec3(0.5, 0.7, 1.0) * t)


if __name__ == "__main__":
    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):
Пример #23
0
def ray_color(r, world):
    hits = world.hit(r, 0, MAX_VAL)
    colors = (hits.normal.normalized() + Vec3(data=(1., 1., 1.))) * .5
    t = 0.5 * (r.direction.normalized().y() + 1.0)
    sky = Vec3(data=(1., 1., 1.)) * (1. - t) + Vec3(data=(0.5, 0.7, 1.0)) * t
    return vec_where(hits.t < MAX_VAL, colors, sky)
from random import SystemRandom
from src.camera import Camera, PositionableCamera
from src.hittable import HitRecord, Hittable, HittableList
from src.material import Dielectric, Lambertian, Metal, ReflectionRecord
from src.ppm import PPM
from src.ray import Ray
from src.sphere import Sphere
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
from typing import List, Optional

import math
import sys

UNIT_VEC3: Vec3 = Vec3(1.0, 1.0, 1.0)
random = SystemRandom()

def color(ray: Ray, world: HittableList, depth: int) -> 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:
        scattering: ReflectionRecord = hit_attempt.material.scatter(ray, hit_attempt)
        # FIXME Is it really worthwhile to check if reflection is not None here?
        # All our Materials assume that a hit has been made, and therefore some
        # reflection should happen (unless it is Vanta).
        if depth < 50 and scattering is not None:
            # Compare this with the hard-coded reflection in 6_matterial.
            return scattering.attenuation * color(scattering.scattering, world, depth + 1)
        else:
            return Vec3(0, 0, 0)
Пример #25
0
from src.ppm import PPM
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3

if __name__ == "__main__":
    hello_world = PPM(300, 200)
    for ri in range(hello_world.height):
        for ci in range(hello_world.width):
            v = Vec3(ri / hello_world.width, ci / hello_world.height, 0.2)
            color = v * 255.99
            color.map(int)
            hello_world.set_pixel(ri, ci, color)
    hello_world.write(_derive_ppm_filename())
Пример #26
0
from src.camera import Camera, PositionableCamera
from src.hittable import HitRecord, Hittable, HittableList
from src.material import Lambertian, ReflectionRecord
from src.ppm import PPM
from src.ray import Ray
from src.sphere import Sphere
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
from typing import List, Optional

import math
import random
import sys

UNIT_VEC3: Vec3 = Vec3(1.0, 1.0, 1.0)


def color(ray: Ray, world: HittableList, depth: int) -> 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:
        reflection: ReflectionRecord = hit_attempt.material.scatter(
            ray, hit_attempt)
        # FIXME Is it really worthwhile to check if reflection is not None here?
        # All our Materials assume that a hit has been made, and therefore some
        # reflection should happen (unless it is Vanta).
        if depth < 50 and reflection is not None:
            # Compare this with the hard-coded reflection in 6_matterial.
            return reflection.attenuation * color(reflection.scattering, world,
Пример #27
0
from src.camera import Camera, PositionableCamera
from src.hittable import HitRecord, Hittable, HittableList
from src.material import Dielectric, Lambertian, Metal, ReflectionRecord
from src.ppm import PPM
from src.ray import Ray
from src.sphere import Sphere
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
from typing import List, Optional

import math
import random
import sys

UNIT_VEC3: Vec3 = Vec3(1, 1, 1)


def random_scene(x_min: int, x_max: int, z_min: int,
                 z_max: int) -> List[Hittable]:
    # Start off the list with the "earth" sphere.
    world: List[Hittable] = [
        #Sphere(Vec3(0, -1000, 0), 1000, Lambertian(Vec3(0.5, 0.5, 0.5)))
    ]

    #for x in range(x_min, x_max):
    #    if len(world) >= 100:
    #        break
    #    for z in range(z_min, z_max):
    #        if len(world) >= 100:
    #            break
    #        material_decider: float = random.random()
Пример #28
0
 def scatter(self, incident_ray: Ray,
             record: "HitRecord") -> ReflectionRecord:
     return ReflectionRecord(Vec3(1, 1, 1), incident_ray)
Пример #29
0
from random import SystemRandom
from src.camera import Camera
from src.hittable import HitRecord, Hittable, HittableList
from src.material import Lambertian, Metal, ReflectionRecord
from src.ppm import PPM
from src.ray import Ray
from src.sphere import Sphere
from src.utils import _derive_ppm_filename
from src.vec3 import Vec3
from typing import List, Optional

import math
import sys

UNIT_VEC3: Vec3 = Vec3(1.0, 1.0, 1.0)
random = SystemRandom()


def color(ray: Ray, world: HittableList, depth: int) -> 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:
        reflection: ReflectionRecord = hit_attempt.material.scatter(
            ray, hit_attempt)
        # FIXME Is it really worthwhile to check if reflection is not None here?
        # All our Materials assume that a hit has been made, and therefore some
        # reflection should happen (unless it is Vanta).
        if depth < 50 and reflection is not None:
            # Compare this with the hard-coded reflection in 6_matterial.
Пример #30
0
 def test_itruediv(self):
     a = Vec3(3, 6, 9)
     lowest_terms_a = Vec3(1, 2, 3)
     a /= 3
     self.assertEqual(lowest_terms_a, a)