2019年5月24日金曜日

開発環境

The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer (Jamis Buck(著)、Pragmatic Bookshelf)、Chapter 11(Reflection and Refraction)のFresnel Effectを取り組んでみる。

コード

Python 3

intersections_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
import math
from intersections import Intersection, Intersections
from tuples import is_equal, Point, Vector, EPSILON
from spheres import Sphere
from planes import Plane
from rays import Ray
from transformations import translation, scaling
from materials import Material


class IntersectionTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_intersection(self):
        s = Sphere()
        i = Intersection(3.5, s)
        self.assertEqual(i.t, 3.5)
        self.assertEqual(i.obj, s)

    def test_prepare_computations(self):
        r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        shape = Sphere()
        i = Intersection(4, shape)
        comps = i.prepare_computations(r)
        for a, b in [(comps.t, i.t),
                     (comps.obj, i.obj),
                     (comps.point, Point(0, 0, -1)),
                     (comps.eye_vector, Vector(0, 0, -1)),
                     (comps.normal_vector, Vector(0, 0, -1))]:
            self.assertEqual(a, b)

    def test_hit_intersection_outside(self):
        r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        shape = Sphere()
        i = Intersection(4, shape)
        comps = i.prepare_computations(r)
        self.assertFalse(comps.inside)

    def test_hit_intersection_inside(self):
        r = Ray(Point(0, 0, 0), Vector(0, 0, 1))
        shape = Sphere()
        i = Intersection(1, shape)
        comps = i.prepare_computations(r)
        for a, b in [(comps.point, Point(0, 0, 1)),
                     (comps.eye_vector, Vector(0, 0, -1)),
                     (comps.normal_vector, Vector(0, 0, -1))]:
            self.assertEqual(a, b)
        self.assertTrue(comps.inside)

    def test_hit_shoud_offset_point(self):
        ray = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        shape = Sphere(transform=translation(0, 0, 1))
        i = Intersection(5, shape)
        comps = i.prepare_computations(ray)
        self.assertLess(comps.over_point.z, -EPSILON / 2)
        self.assertGreater(comps.point.z, comps.over_point.z)

    def test_precomputing_reflection_vvector(self):
        shape = Plane()
        ray = Ray(Point(0, 1, -1),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        intersection = Intersection(math.sqrt(2), shape)
        comps = intersection.prepare_computations(ray)
        self.assertEqual(comps.reflect_vector,
                         Vector(0, 1 / math.sqrt(2), 1 / math.sqrt(2)))

    def test_n1_n2_various_intersections(self):
        a = glass_sphere()
        a.transform = scaling(2, 2, 2)
        a.material.refractive_index = 1.5
        b = glass_sphere()
        b.transform = translation(0, 0, -0.25)
        b.material.refractive_index = 2
        c = glass_sphere()
        c.transofrm = translation(0, 0, 0.25)
        c.material.refractive_index = 2.5
        ray = Ray(Point(0, 0, -4), Vector(0, 0, 1))
        ts = [(2, a), (2.75, b), (3.25, c), (4.75, b), (5.25, c), (6, a)]
        intersections = Intersections(*[Intersection(*t) for t in ts])
        ns = [(1.0, 1.5),
              (1.5, 2.0),
              (2.0, 2.5),
              (2.5, 2.5),
              (2.5, 1.5),
              (1.5, 1.0)]
        for i, (n1, n2) in enumerate(ns):
            comps = intersections[i].prepare_computations(ray, intersections)
            self.assertEqual(comps.n1, n1)
            self.assertEqual(comps.n2, n2)

    def test_under_point_is_offset_below_surface(self):
        ray = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        shape = glass_sphere()
        shape.transform = translation(0, 0, 1)
        intersection = Intersection(5, shape)
        intersections = Intersections(intersection)
        comps = intersection.prepare_computations(ray, intersections)
        self.assertGreater(comps.under_point.z, EPSILON / 2)
        self.assertLess(comps.point.z, comps.under_point.z)

    def test_schlick_approximation_under_total_internal_reflection(self):
        shape = glass_sphere()
        ray = Ray(Point(0, 0, 1 / math.sqrt(2)), Vector(0, 1, 0))
        intersections = Intersections(
            *[Intersection(t, shape)
              for t in [-1 / math.sqrt(2), 1 / math.sqrt(2)]])
        computations = intersections[1].prepare_computations(ray,
                                                             intersections)
        reflectance = computations.schlick()
        self.assertEqual(reflectance, 1.0)

    def test_schlick_approximation_with_perpendicular_viewing_angle(self):
        shape = glass_sphere()
        ray = Ray(Point(0, 0, 0), Vector(0, 1, 0))
        intersections = Intersections(
            *[Intersection(t, shape) for t in [-1, 1]])
        computations = intersections[1].prepare_computations(
            ray, intersections)
        reflectance = computations.schlick()
        self.assertTrue(is_equal(reflectance, 0.04))

    def test_schlick_approximation_with_small_angle_and_n2_gt_n1(self):
        shape = glass_sphere()
        ray = Ray(Point(0, 0.99, -2), Vector(0, 0, 1))
        intersections = Intersections(Intersection(1.8589, shape))
        computations = intersections[0].prepare_computations(ray,
                                                             intersections)
        reflectance = computations.schlick()
        self.assertTrue(is_equal(reflectance, 0.48873))


def glass_sphere():
    return Sphere(material=Material(transparency=1.0, refractive_index=1.5))


class IntersectionsTest(TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_intersection(self):
        s = Sphere()
        i1 = Intersection(1, s)
        i2 = Intersection(2, s)
        xs = Intersections(i1, i2)
        for a, b in [(len(xs), 2), (xs[0].t, 1), (xs[1].t, 2)]:
            self.assertEqual(a, b)

    def test_hit(self):
        s = Sphere()
        i1 = Intersection(1, s)
        i2 = Intersection(2, s)
        xs = Intersections(i2, i1)
        self.assertEqual(xs.hit(), i1)

    def test_hit_positive_and_negative(self):
        s = Sphere()
        i1 = Intersection(-1, s)
        i2 = Intersection(1, s)
        xs = Intersections(i2, i1)
        self.assertEqual(xs.hit(), i2)

    def test_hit_none(self):
        s = Sphere()
        i1 = Intersection(-2, s)
        i2 = Intersection(-1, s)
        xs = Intersections(i2, i1)
        self.assertIsNone(xs.hit())

    def test_hit_nonnegative(self):
        s = Sphere()
        intersections = [Intersection(t, s) for t in [5, 7, -3, 2]]
        xs = Intersections(*intersections)
        self.assertEqual(xs.hit(), intersections[-1])


if __name__ == '__main__':
    main()

intersections.py

import math
from tuples import Point, EPSILON


class Intersection:
    def __init__(self, t: float, obj):
        self.t = t
        self.obj = obj

    def __repr__(self):
        return f'Intersection({self.t},{self.obj})'

    def prepare_computations(self, ray, intersections=None):
        if intersections is None:
            intersections = []

        point = ray.position(self.t)
        eye_vector = -ray.direction
        normal_vector = self.obj.normal_at(point)

        objs = []
        n1 = 1
        n2 = 1
        for intersection in intersections:
            if intersection == self:
                if not objs:
                    n1 = 1
                else:
                    n1 = objs[-1].material.refractive_index
            if intersection.obj in objs:
                objs.remove(intersection.obj)
            else:
                objs.append(intersection.obj)
            if intersection == self:
                if not objs:
                    n2 = 1
                else:
                    n2 = objs[-1].material.refractive_index
                break
        return Computations(t=self.t,
                            obj=self.obj,
                            point=point,
                            eye_vector=eye_vector,
                            normal_vector=normal_vector,
                            ray=ray,
                            n1=n1,
                            n2=n2)


class Intersections:
    def __init__(self, *args):
        self.xs = list(args)
        self.xs.sort(key=lambda o: o.t)

    def __getitem__(self, i: int):
        return self.xs[i]

    def __len__(self):
        return len(self.xs)

    def __repr__(self):
        return f'Inersections({self.xs})'

    def hit(self):
        for i in self.xs:
            if i.t > 0:
                return i
        return None


class Computations:
    def __init__(self, t, obj, point, eye_vector, normal_vector, ray, n1, n2):
        self.t = t
        self.obj = obj
        self.point = point
        self.eye_vector = eye_vector
        self.normal_vector = normal_vector
        if normal_vector.dot(eye_vector) < 0:
            self.inside = True
            self.normal_vector = -self.normal_vector
        else:
            self.inside = False
        self.over_point = point + self.normal_vector * EPSILON
        self.under_point = point - self.normal_vector * EPSILON
        self.reflect_vector = ray.direction.reflect(self.normal_vector)
        self.n1 = n1
        self.n2 = n2

    def __repr__(self):
        return f'Computations({self.t},{self.obj},{self.point},' +\
            f'{self.eye_vector},{self.normal_vector},{self.inside},' + \
            f'{self.over_point},{self.reflect_vector},{self.n1},{self.n2})'

    def schlick(self):
        cos = self.eye_vector.dot(self.normal_vector)
        if self.n1 > self.n2:
            n = self.n1 / self.n2
            sin2t = n ** 2 * (1 - cos ** 2)
            if sin2t > 1:
                return 1
            cos = math.sqrt(1 - sin2t)
        r = ((self.n1 - self.n2) / (self.n1 + self.n2)) ** 2
        return r + (1 - r) * (1 - cos) ** 5

world_test.py

#!/usr/bin/env python3
from unittest import TestCase, main
import math
from tuples import Point, Vector, Color
from lights import Light
from spheres import Sphere
from planes import Plane
from materials import Material
from patterns import Pattern
from transformations import scaling, translation
from rays import Ray
from intersections import Intersection, Intersections
from world import World


class WorldTest(TestCase):
    def setUp(self):
        self.light = Light(Point(-10, 10, -10), Color(1, 1, 1))
        self.sphere1 = Sphere(material=Material(color=Color(0.8, 1.0, 0.6),
                                                diffuse=0.7,
                                                specular=0.2))
        self.sphere2 = Sphere(transform=scaling(0.5, 0.5, 0.5))
        self.world = World(objs=[self.sphere1, self.sphere2], light=self.light)

    def tearDown(self):
        pass

    def test_world(self):
        w = World()
        self.assertEqual(len(w), 0)
        self.assertIsNone(w.light)

    def test_default_world(self):
        self.assertEqual(self.world.light, self.light)
        for s in [self.sphere1, self.sphere2]:
            self.assertIn(s, self.world)

    def test_intersect_ray(self):
        r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        xs = self.world.intersect(r)
        self.assertEqual(len(xs), 4)
        for i, t in enumerate([4, 4.5, 5.5, 6]):
            self.assertEqual(xs[i].t, t)

    def test_shade_hit(self):
        r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        shape = self.world[0]
        i = Intersection(4, shape)
        comps = i.prepare_computations(r)
        c = self.world.shade_hit(comps)
        self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))

    def test_shade_hit_from_inside(self):
        self.world.light = Light(Point(0, 0.25, 0), Color(1, 1, 1))
        r = Ray(Point(0, 0, 0), Vector(0, 0, 1))
        shape = self.world[1]
        i = Intersection(0.5, shape)
        comps = i.prepare_computations(r)
        c = self.world.shade_hit(comps)
        self.assertEqual(c, Color(0.90498, 0.90498, 0.90498))

    def test_color_ray_misses(self):
        r = Ray(Point(0, 0, -5), Vector(0, 1, 0))
        c = self.world.color_at(r)
        self.assertEqual(c, Color(0, 0, 0))

    def test_color_ray_hits(self):
        r = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        c = self.world.color_at(r)
        self.assertEqual(c, Color(0.38066, 0.47583, 0.2855))

    def test_color_intersection_behind_the_ray(self):
        outer = self.world[0]
        inner = self.world[1]
        outer.material.ambient = 1
        inner.material.ambient = 1
        r = Ray(Point(0, 0, 0.75), Vector(0, 0, -1))
        c = self.world.color_at(r)
        self.assertNotEqual(c, outer.material.color)
        self.assertEqual(c, inner.material.color)

    def test_no_shadow_nothing_is_collinear_with_point_and_light(self):
        p = Point(0, 10, 0)
        self.assertFalse(self.world.is_shadowed(p))

    def test_shadow_obj_between_point_and_light(self):
        p = Point(10, -10, 10)
        self.assertTrue(self.world.is_shadowed(p))

    def test_no_shadow_obj_behind_light(self):
        p = Point(-20, 20, -20)
        self.assertFalse(self.world.is_shadowed(p))

    def test_no_shadow_obj_behind_point(self):
        p = Point(-2, 2, -2)
        self.assertFalse(self.world.is_shadowed(p))

    def test_shade_hit_given_intersection_in_shadow(self):
        light = Light(Point(0, 0, -10), Color(1, 1, 1))
        s1 = Sphere()
        s2 = Sphere(translation(0, 0, 10))
        w = World([s1, s2], light)
        ray = Ray(Point(0, 0, 5), Vector(0, 0, 1))
        i = Intersection(4, s2)
        comps = i.prepare_computations(ray)
        c = w.shade_hit(comps)
        self.assertEqual(c, Color(0.1, 0.1, 0.1))

    def test_reflected_color_for_nonreflecive_material(self):
        ray = Ray(Point(0, 0, 0), Vector(0, 0, 1))
        shape = self.sphere2
        shape.material.ambient = 1
        intersection = Intersection(1, shape)
        comps = intersection.prepare_computations(ray)
        color = self.world.reflected_color(comps)
        self.assertEqual(color, Color(0, 0, 0))

    def test_reflected_color_for_reflective_material(self):
        shape = Plane(material=Material(reflective=0.5),
                      transform=translation(0, -1, 0))
        self.world.objs.append(shape)
        ray = Ray(Point(0, 0, -3),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        intersection = Intersection(math.sqrt(2), shape)
        comps = intersection.prepare_computations(ray)
        color = self.world.reflected_color(comps)
        self.assertEqual(color, Color(0.19033, 0.23791, 0.14274))

    def test_shade_hit_with_reflective_material(self):
        shape = Plane(material=Material(reflective=0.5),
                      transform=translation(0, -1, 0))
        self.world.objs.append(shape)
        ray = Ray(Point(0, 0, -3),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        intersection = Intersection(math.sqrt(2), shape)
        comps = intersection.prepare_computations(ray)
        color = self.world.shade_hit(comps)
        self.assertEqual(color, Color(0.87675, 0.92434, 0.82917))

    def test_reflected_color_at_maximum_recursive_depth(self):
        shape = Plane(material=Material(reflective=0.5),
                      transform=translation(0, -1, 0))
        self.world.objs.append(shape)
        ray = Ray(Point(0, 0, -3),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        intersection = Intersection(math.sqrt(2), shape)
        comps = intersection.prepare_computations(ray)
        color = self.world.reflected_color(comps, 0)
        self.assertEqual(color, Color(0, 0, 0))

    def test_refracted_color_with_opaque_surface(self):
        shape = self.world[0]
        ray = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        intersections = Intersections(
            *[Intersection(t, shape) for t in [4, 6]])
        computations = intersections[0].prepare_computations(ray,
                                                             intersections)
        color = self.world.refracted_color(computations, 5)
        self.assertEqual(color, Color(0, 0, 0))

    def test_refracted_color_at_maximum_recursive_depth(self):
        shape = self.world[0]
        shape.material.transparency = 1.0
        shape.material.refractive_index = 1.5
        ray = Ray(Point(0, 0, -5), Vector(0, 0, 1))
        intersections = Intersections(*[Intersection(t, shape)
                                        for t in [4, 6]])
        computations = intersections[0].prepare_computations(ray,
                                                             intersections)
        color = self.world.refracted_color(computations, 0)
        self.assertEqual(color, Color(0, 0, 0))

    def test_refracted_color_under_total_internal_reflection(self):
        shape = self.world[0]
        shape.material.transparency = 1.0
        shape.material.refractive_index = 1.5
        ray = Ray(Point(0, 0, 1 / math.sqrt(2)), Vector(0, 1, 0))
        intersections = Intersections(*[Intersection(t, shape)
                                        for t in
                                        [-1 / math.sqrt(2), 1 / math.sqrt(2)]])
        computations = intersections[1].prepare_computations(ray,
                                                             intersections)
        color = self.world.refracted_color(computations, 5)
        self.assertEqual(color, Color(0, 0, 0))

    def test_refracted_color_with_refracted_ray(self):
        a = self.world[0]
        a.material.ambient = 1.0
        a.material.pattern = Pattern()
        b = self.world[1]
        b.material.transparency = 1
        b.material.refractive_index = 1.5
        ray = Ray(Point(0, 0, 0.1), Vector(0, 1, 0))
        ts = [(-0.9899, a), (-0.4899, b), (0.4899, b), (0.9899, a)]
        intersections = Intersections(
            *[Intersection(t, shape) for t, shape in ts])
        computations = intersections[2].prepare_computations(ray,
                                                             intersections)
        color = self.world.refracted_color(computations, 5)
        self.assertEqual(color, Color(0, 0.99888, 0.04721))

    def test_shade_hit_with_transpraent_material(self):
        floor = Plane(transform=translation(0, -1, 0),
                      material=Material(transparency=0.5, refractive_index=1.5))
        ball = Sphere(material=Material(color=Color(1, 0, 0),
                                        ambient=0.5),
                      transform=translation(0, -3.5, -0.5))
        self.world.objs += [floor, ball]
        ray = Ray(Point(0, 0, -3),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        intersections = Intersections(Intersection(math.sqrt(2), floor))
        computations = intersections[0].prepare_computations(
            ray, intersections)
        color = self.world.shade_hit(computations, 5)
        self.assertEqual(color, Color(0.93642, 0.68642, 0.68642))

    def test_shade_hit_with_reflective_transparent_material(self):
        ray = Ray(Point(0, 0, -3),
                  Vector(0, -1 / math.sqrt(2), 1 / math.sqrt(2)))
        floor = Plane(transform=translation(0, -1, 0),
                      material=Material(reflective=0.5,
                                        transparency=0.5,
                                        refractive_index=1.5))
        ball = Sphere(transform=translation(0, -3.5, -0.5),
                      material=Material(Color(1, 0, 0),
                                        ambient=0.5))
        self.world.objs.extend([floor, ball])
        intersections = Intersections(Intersection(math.sqrt(2), floor))
        computations = intersections[0].prepare_computations(ray,
                                                             intersections)
        color = self.world.shade_hit(computations, 5)
        self.assertEqual(color, Color(0.93391, 0.69643, 0.69243))


if __name__ == '__main__':
    main()

world.py

#!/usr/bin/env python3
import math
from intersections import Intersections
from tuples import Color
from rays import Ray


class World:
    def __init__(self, objs=None, light=None):
        if objs is None:
            self.objs = []
        else:
            self.objs = objs
        self.light = light

    def __getitem__(self, y):
        return self.objs[y]

    def __cointains__(self, key):
        return key in self.objs

    def __len__(self):
        return len(self.objs)

    def __repr__(self):
        return f'World({self.objs}, {self.light})'

    def intersect(self, ray):
        intersections = []
        for obj in self.objs:
            intersections += obj.intersect(ray)
        return Intersections(*intersections)

    def shade_hit(self, computations, remaining=1):
        surface = computations.obj.material.lighting(
            computations.obj, self.light, computations.point,
            computations.eye_vector, computations.normal_vector,
            self.is_shadowed(computations.over_point))
        reflected = self.reflected_color(computations, remaining)
        refracted = self.refracted_color(computations, remaining)

        material = computations.obj.material
        if material.reflective > 0 and material.transparency > 0:
            reflectance = computations.schlick()
            return surface + reflected * reflectance + \
                refracted * (1 - reflectance)
        return surface + reflected + refracted

    def color_at(self, ray, remaining=1):
        intersections = self.intersect(ray)
        hit = intersections.hit()
        if hit is None:
            return Color(0, 0, 0)
        comps = hit.prepare_computations(ray)
        return self.shade_hit(comps, remaining)

    def is_shadowed(self, point) -> bool:
        vector = self.light.position - point
        distance = vector.magnitude()
        direction = vector.normalize()
        ray = Ray(point, direction)
        intersections = self.intersect(ray)
        hit = intersections.hit()
        return (hit is not None) and hit.t < distance

    def reflected_color(self, comps, remaining=5):
        if remaining <= 0:
            return Color(0, 0, 0)
        if comps.obj.material.reflective == 0:
            return Color(0, 0, 0)
        reflect_ray = Ray(comps.over_point, comps.reflect_vector)
        color = self.color_at(reflect_ray, remaining)
        return color * comps.obj.material.reflective

    def refracted_color(self, computations, remaining):
        if remaining == 0 or computations.obj.material.transparency == 0:
            return Color(0, 0, 0)
        n_ratio = computations.n1 / computations.n2
        cos_i = computations.eye_vector.dot(computations.normal_vector)
        sin2t = n_ratio ** 2 * (1 - cos_i ** 2)
        if sin2t > 1:
            return Color(0, 0, 0)
        cos_t = math.sqrt(1 - sin2t)
        direction = computations.normal_vector * (n_ratio * cos_i - cos_t) - \
            computations.eye_vector * n_ratio
        refract_ray = Ray(computations.under_point, direction)
        color = self.color_at(refract_ray, remaining-1) * \
            computations.obj.material.transparency
        return color

sample3.py

#!/usr/bin/env python3
import math
import time
from tuples import Point, Vector, Color
from planes import Plane
from spheres import Sphere
from materials import Material
from patterns import Solid, Checkers
from camera import Camera
from lights import Light
from world import World
from transformations import translation, scaling, rotation_x, rotation_y
from transformations import view_transform
print('ファイル名, rendering time(秒)')

width = 250
height = 125

light = Light(Point(-10, 10, -10), Color(1, 1, 1))
wall1 = Plane(transform=translation(0, 0, 10) *
              rotation_y(math.pi / 4) *
              rotation_x(math.pi / 2),
              material=Material(Color(1, 0, 0)))
wall2 = Plane(transform=translation(0, 0, 10) *
              rotation_y(-math.pi / 4) *
              rotation_x(math.pi / 2),
              material=Material(Color(0, 1, 0)))
floor = Plane(transform=translation(0, -1, 0),
              material=Material(reflective=0.5,
                                transparency=0.5,
                                refractive_index=1.5))
ball1 = Sphere(transform=translation(0, -3.5, 5),
               material=Material(Color(0, 0, 1),
                                 ambient=0.9))
ball2 = Sphere(transform=translation(0, -3.5, -1.5),
               material=Material(Color(0, 0, 1),
                                 ambient=0.9))
world = World([wall1, wall2, floor, ball1, ball2], light=light)

camera = Camera(width, height, math.pi / 2,
                transform=view_transform(
                    Point(0, 1.5, -10),
                    Point(0, 0, 0),
                    Vector(0, 1, 0)))

start = time.time()
canvas = camera.render(world)
s = time.time() - start
with open(f'sample4.ppm', 'w') as f:
    canvas.to_ppm(f)
print(f'sample4.ppm,{s}')

入出力結果(Bash、cmd(コマンドプロンプト)、Terminal、Jupyter(IPython))

C:\Users\...>py intersections_test.py
................
----------------------------------------------------------------------
Ran 16 tests in 0.017s

OK

C:\Users\...>py world_test.py
.......................
----------------------------------------------------------------------
Ran 23 tests in 0.069s

OK

C:\Users\...>py sample3.py
ファイル名, rendering time(秒)
sample4.ppm,418.6385991573334

C:\Users\...>

0 コメント:

コメントを投稿