def main() -> None:
    aspect_ratio = 16 / 9
    image_width = 720
    image_height = int(image_width / aspect_ratio)
    samples_per_pixel = 48
    max_depth = 5

    world = random_scene()

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus = 10
    aperture = 0.1
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus)

    print("Start rendering.")
    start_time = time.time()

    img_list = Parallel(n_jobs=2, verbose=20)(
        delayed(scan_frame)(world, cam, image_width, image_height, max_depth)
        for s in range(samples_per_pixel))

    end_time = time.time()
    print(f"\nDone. Total time: {round(end_time - start_time, 1)} s.")

    final_img = Img(image_width, image_height)
    for img in img_list:
        final_img.write_frame(img)
    final_img.average(samples_per_pixel).gamma(2)
    final_img.save("./output.png", True)
Beispiel #2
0
def main() -> None:
    aspect_ratio = 16 / 9
    image_width = 256
    image_height = int(image_width / aspect_ratio)
    samples_per_pixel = 20
    max_depth = 10

    world: HittableList = three_ball_scene()

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0.1
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus)

    print("Start rendering.")
    start_time = time.time()

    n_processer = multiprocessing.cpu_count()
    img_list: List[Img] = Parallel(n_jobs=n_processer)(
        delayed(scan_line)(j, world, cam, image_width, image_height,
                           samples_per_pixel, max_depth)
        for j in range(image_height - 1, -1, -1))

    final_img = Img(image_width, image_height)
    final_img.set_array(np.concatenate([img.frame for img in img_list]))

    end_time = time.time()
    print(f"\nDone. Total time: {round(end_time - start_time, 1)} s.")
    final_img.save("./output.png", True)
Beispiel #3
0
    def __init__(self, obj: Hittable, angle: float) -> None:
        radians = degrees_to_radians(angle)
        self.sin_theta = np.sin(radians)
        self.cos_theta = np.cos(radians)
        self.obj = obj
        self.bbox = obj.bounding_box(0, 1)

        if self.bbox is None:
            raise ValueError

        point_min = Point3(np.inf, np.inf, np.inf)
        point_max = Point3(-np.inf, -np.inf, -np.inf)

        for i in range(2):
            for j in range(2):
                for k in range(2):
                    x = i * self.bbox.max().x() + (1 - i) * self.bbox.min().x()
                    y = j * self.bbox.max().y() + (1 - j) * self.bbox.min().y()
                    z = k * self.bbox.max().z() + (1 - k) * self.bbox.min().z()

                    newx = self.cos_theta * x + self.sin_theta * z
                    newz = -self.sin_theta * x + self.cos_theta * z

                    new = Vec3(newx, y, newz)
                    for c in range(3):
                        point_min[c] = np.minimum(point_min[c], new[c])
                        point_max[c] = np.maximum(point_max[c], new[c])

        self.bbox = AABB(point_min, point_max)
Beispiel #4
0
def main() -> None:
    aspect_ratio = 16 / 9
    image_width = 256
    image_height = int(image_width / aspect_ratio)
    samples_per_pixel = 20
    max_depth = 10

    world: HittableList = three_ball_scene()

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0.1
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus)

    print("Start rendering.")
    start_time = time.time()

    n_processer = multiprocessing.cpu_count()
    img_list: List[Img] = Parallel(n_jobs=n_processer, verbose=10)(
        delayed(scan_line)(j, world, cam, image_width, image_height,
                           samples_per_pixel, max_depth)
        for j in range(image_height - 1, -1, -1))

    # # Profile prologue
    # import cProfile
    # import pstats
    # import io
    # from pstats import SortKey
    # pr = cProfile.Profile()
    # pr.enable()

    # img_list: List[Img] = list()
    # for j in range(image_height-1, -1, -1):
    #     img_list.append(
    #         scan_line(
    #             j, world, cam,
    #             image_width, image_height,
    #             samples_per_pixel, max_depth
    #         )
    #     )

    # # Profile epilogue
    # pr.disable()
    # s = io.StringIO()
    # sortby = SortKey.CUMULATIVE
    # ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
    # ps.print_stats()
    # print(s.getvalue())

    final_img = Img(image_width, image_height)
    final_img.set_array(np.concatenate([img.frame for img in img_list]))

    end_time = time.time()
    print(f"\nDone. Total time: {round(end_time - start_time, 1)} s.")
    final_img.save("./output.png", True)
Beispiel #5
0
def three_ball_scene() -> HittableList:
    world = HittableList()
    world.add(Sphere(Point3(0, 0, -1), 0.5, Lambertian(Color(0.1, 0.2, 0.5))))
    world.add(
        Sphere(Point3(0, -100.5, -1), 100, Lambertian(Color(0.8, 0.8, 0))))
    world.add(Sphere(Point3(1, 0, -1), 0.5, Metal(Color(0.8, 0.6, 0.2), 0.3)))
    world.add(Sphere(Point3(-1, 0, -1), 0.5, Dielectric(1.5)))
    world.add(Sphere(Point3(-1, 0, -1), -0.45, Dielectric(1.5)))
    return world
Beispiel #6
0
def main() -> None:
    aspect_ratio = 16 / 9
    image_width = 1920
    image_height = int(image_width / aspect_ratio)
    samples_per_pixel = 48
    max_depth = 10

    world: HittableList = random_scene()

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0.1
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus)

    print("Start rendering.")
    start_time = time.time()

    img_list: List[Vec3List] = Parallel(n_jobs=4, verbose=20)(
        delayed(scan_frame)(world, cam, image_width, image_height, max_depth)
        for s in range(samples_per_pixel))

    # # Profile prologue
    # import cProfile
    # import pstats
    # import io
    # from pstats import SortKey
    # pr = cProfile.Profile()
    # pr.enable()

    # img_list: List[Vec3List] = list()
    # for sample_num in range(samples_per_pixel):
    #     img_list.append(
    #         scan_frame(world, cam, image_width, image_height, max_depth)
    #     )

    # # Profile epilogue
    # pr.disable()
    # s = io.StringIO()
    # sortby = SortKey.CUMULATIVE
    # ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
    # ps.print_stats()
    # print(s.getvalue())

    end_time = time.time()
    print(f"\nDone. Total time: {round(end_time - start_time, 1)} s.")

    final_img = Img(image_width, image_height)
    for img in img_list:
        final_img.write_frame(img)
    final_img.average(samples_per_pixel).gamma(2).up_side_down()
    final_img.save("./output.png", True)
Beispiel #7
0
 def surrounding_box(box0: AABB, box1: AABB) -> AABB:
     small = Point3(min(box0.min().x(),
                        box1.min().x()), min(box0.min().y(),
                                             box1.min().y()),
                    min(box0.min().z(),
                        box1.min().z()))
     big = Point3(max(box0.max().x(),
                      box1.max().x()), max(box0.max().y(),
                                           box1.max().y()),
                  max(box0.max().z(),
                      box1.max().z()))
     return AABB(small, big)
def three_ball_scene():
    world = HittableList()
    world.add(
        Sphere(Point3(0, 0, -1), 0.5, Lambertian(Color(0.1, 0.2, 0.5), 1)))
    world.add(
        Sphere(Point3(0, -100.5, -1), 100, Lambertian(Color(0.8, 0.8, 0), 2)))
    world.add(
        Sphere(Point3(1, 0, -1), 0.5, Metal(Color(0.8, 0.6, 0.2), 0.3, 3)))
    material_dielectric = Dielectric(1.5, 4)
    world.add(Sphere(Point3(-1, 0, -1), 0.5, material_dielectric))
    world.add(Sphere(Point3(-1, 0, -1), -0.45, material_dielectric))
    return world
Beispiel #9
0
 def __init__(self,
              origin: Point3 = Point3(),
              direction: Vec3 = Vec3(),
              time: float = 0) -> None:
     self.orig = origin
     self.dir = direction
     self.tm = time
Beispiel #10
0
def three_ball_scene(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()
    world.add(
        Sphere(Point3(0, 0, -1), 0.5, Lambertian(SolidColor(0.1, 0.2, 0.5))))
    world.add(
        Sphere(Point3(0, -100.5, -1), 100, Lambertian(SolidColor(0.8, 0.8,
                                                                 0))))
    world.add(Sphere(Point3(1, 0, -1), 0.5, Metal(Color(0.8, 0.6, 0.2), 0.3)))
    world.add(Sphere(Point3(-1, 0, -1), 0.5, Dielectric(1.5)))
    world.add(Sphere(Point3(-1, 0, -1), -0.45, Dielectric(1.5)))
    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(3, 3, 2)
    lookat = Point3(0, 0, -1)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = (lookfrom - lookat).length()
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    # lookfrom = Point3(13, 2, 3)
    # lookat = Point3(0, 0, 0)
    # vup = Vec3(0, 1, 0)
    # vfov = 20
    # dist_to_focus: float = 10
    # aperture: float = 0.1
    # cam = Camera(
    #     lookfrom, lookat, vup, vfov, aspect_ratio, aperture, dist_to_focus,
    #     time0, time1
    # )

    return world_bvh, cam
Beispiel #11
0
def two_perlin_spheres(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    pertext = NoiseTexture(4)
    world.add(Sphere(Point3(0, -1000, 0), 1000, Lambertian(pertext)))
    world.add(Sphere(Point3(0, 2, 0), 2, Lambertian(pertext)))

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #12
0
    def turb(self, p: Point3, depth: int = 7) -> float:
        accum: float = 0
        weight: float = 1
        temp_p = p.copy()

        for i in range(depth):
            accum += weight * self.noise(temp_p)
            weight *= 0.5
            temp_p *= 2

        return np.abs(accum)
Beispiel #13
0
def random_scene(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    # ground_material = Lambertian(SolidColor(0.5, 0.5, 0.5))
    ground_material = Lambertian(
        CheckerTexture(SolidColor(0.2, 0.3, 0.1), SolidColor(0.9, 0.9, 0.9)))
    world.add(Sphere(Point3(0, -1000, 0), 1000, ground_material))

    for a in range(-11, 11):
        for b in range(-11, 11):
            choose_mat = random_float()
            center = Point3(a + 0.9 * random_float(), 0.2,
                            b + 0.9 * random_float())

            if (center - Vec3(4, 0.2, 0)).length() > 0.9:
                if choose_mat < 0.6:
                    # Diffuse
                    albedo = Color.random() * Color.random()
                    sphere_material_diffuse = Lambertian(SolidColor(albedo))
                    center2 = center + Vec3(0, random_float(0, 0.5), 0)
                    world.add(
                        MovingSphere(center, center2, 0, 1, 0.2,
                                     sphere_material_diffuse))
                elif choose_mat < 0.8:
                    # Metal
                    albedo = Color.random(0.5, 1)
                    fuzz = random_float(0, 0.5)
                    sphere_material_metal = Metal(albedo, fuzz)
                    world.add(Sphere(center, 0.2, sphere_material_metal))
                else:
                    # Glass
                    sphere_material_glass = Dielectric(1.5)
                    world.add(Sphere(center, 0.2, sphere_material_glass))

    material_1 = Dielectric(1.5)
    world.add(Sphere(Point3(0, 1, 0), 1, material_1))

    material_2 = Lambertian(SolidColor(0.4, 0.2, 0.1))
    world.add(Sphere(Point3(-4, 1, 0), 1, material_2))

    material_3 = Metal(Color(0.7, 0.6, 0.5), 0)
    world.add(Sphere(Point3(4, 1, 0), 1, material_3))

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0.1
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #14
0
def two_spheres(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    checker = CheckerTexture(SolidColor(0.2, 0.3, 0.1),
                             SolidColor(0.9, 0.9, 0.9))
    world.add(Sphere(Point3(0, -10, 0), 10, Lambertian(checker)))
    world.add(Sphere(Point3(0, 10, 0), 10, Lambertian(checker)))

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(13, 2, 3)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #15
0
def earth(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    earth_texture = ImageTexture("earthmap.jpg")
    earth_surface = Lambertian(earth_texture)
    globe = Sphere(Point3(0, 0, 0), 2, earth_surface)

    world.add(globe)
    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(0, 0, -5)
    lookat = Point3(0, 0, 0)
    vup = Vec3(0, 1, 0)
    vfov = 50
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #16
0
def random_scene() -> HittableList:
    world = HittableList()

    ground_material = Lambertian(Color(0.5, 0.5, 0.5), 1)
    world.add(Sphere(Point3(0, -1000, 0), 1000, ground_material))

    sphere_material_glass = Dielectric(1.5, 2)
    for a in range(-11, 11):
        for b in range(-11, 11):
            choose_mat = random_float()
            center = Point3(a + 0.9 * random_float(), 0.2,
                            b + 0.9 * random_float())

            if (center - Vec3(4, 0.2, 0)).length() > 0.9:
                idx = (a * 22 + b) + (11 * 22 + 11) + 6
                if choose_mat < 0.6:
                    # Diffuse
                    albedo = Color.random() * Color.random()
                    sphere_material_diffuse = Lambertian(albedo, idx)
                    world.add(Sphere(center, 0.2, sphere_material_diffuse))
                elif choose_mat < 0.8:
                    # Metal
                    albedo = Color.random(0.5, 1)
                    fuzz = random_float(0, 0.5)
                    sphere_material_metal = Metal(albedo, fuzz, idx)
                    world.add(Sphere(center, 0.2, sphere_material_metal))
                else:
                    # Glass
                    world.add(Sphere(center, 0.2, sphere_material_glass))

    material_1 = Dielectric(1.5, 3)
    world.add(Sphere(Point3(0, 1, 0), 1, material_1))

    material_2 = Lambertian(Color(0.4, 0.2, 0.1), 4)
    world.add(Sphere(Point3(-4, 1, 0), 1, material_2))

    material_3 = Metal(Color(0.7, 0.6, 0.5), 0, 5)
    world.add(Sphere(Point3(4, 1, 0), 1, material_3))

    return world
Beispiel #17
0
    def noise(self, p: Point3) -> float:
        u = p.x() - np.floor(p.x())
        v = p.y() - np.floor(p.y())
        w = p.z() - np.floor(p.z())

        i = int(np.floor(p.x()))
        j = int(np.floor(p.y()))
        k = int(np.floor(p.z()))
        c: List[List[List[Vec3]]] = np.empty((2, 2, 2), dtype=object)

        for di in range(2):
            for dj in range(2):
                for dk in range(2):
                    c[di][dj][dk] = self.ranvec[self.perm_x[(i + di) & 255]
                                                ^ self.perm_y[(j + dj) & 255]
                                                ^ self.perm_z[(k + dk) & 255]]

        return Perlin.trilinear_interp(c, u, v, w)
Beispiel #18
0
def simple_light(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    pertext = NoiseTexture(4)
    world.add(Sphere(Point3(0, -1000, 0), 1000, Lambertian(pertext)))
    world.add(Sphere(Point3(0, 2, 0), 2, Lambertian(pertext)))

    difflight = DiffuseLight(SolidColor(4, 4, 4))
    world.add(Sphere(Point3(0, 7, 0), 2, difflight))
    world.add(XYRect(3, 5, 1, 3, -2, difflight))

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(23, 4, 5)
    lookat = Point3(0, 2, 0)
    vup = Vec3(0, 1, 0)
    vfov = 20
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #19
0
def cornell_box(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    # Colors
    red = Lambertian(SolidColor(0.65, 0.05, 0.05))
    white = Lambertian(SolidColor(0.73, 0.73, 0.73))
    green = Lambertian(SolidColor(0.12, 0.45, 0.15))
    light = DiffuseLight(SolidColor(15, 15, 15))

    # Outer box
    world.add(FlipFace(YZRect(0, 555, 0, 555, 555, green)))
    world.add(YZRect(0, 555, 0, 555, 0, red))
    world.add(XZRect(213, 343, 227, 332, 554, light))
    world.add(XZRect(0, 555, 0, 555, 0, white))
    world.add(FlipFace(XZRect(0, 555, 0, 555, 555, white)))
    world.add(FlipFace(XYRect(0, 555, 0, 555, 555, white)))

    # Objects in the box
    box1: Hittable = Box(Vec3(0, 0, 0), Point3(165, 330, 165), white)
    box1 = RotateY(box1, 15)
    box1 = Translate(box1, Point3(265, 0, 295))
    box1 = ConstantMedium(box1, 0.01, SolidColor(0, 0, 0))
    world.add(box1)

    box2: Hittable = Box(Point3(0, 0, 0), Point3(165, 165, 165), white)
    box2 = RotateY(box2, -18)
    box2 = Translate(box2, Point3(130, 0, 65))
    box2 = ConstantMedium(box2, 0.01, SolidColor(1, 1, 1))
    world.add(box2)

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(278, 278, -800)
    lookat = Point3(278, 278, 0)
    vup = Vec3(0, 1, 0)
    vfov = 40
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam
Beispiel #20
0
 def __init__(
     self, _min: Point3 = Point3(), _max: Point3 = Point3()) -> None:
     self._min = _min
     self._max = _max
Beispiel #21
0
 def bounding_box(self, t0: float, t1: float) -> Optional[AABB]:
     output_box = AABB(
         Point3(self.k-0.0001, self.y0, self.z0),
         Point3(self.k+0.0001, self.y1, self.z1)
     )
     return output_box
Beispiel #22
0
 def __init__(
     self, origin: Point3 = Point3(), direction: Vec3 = Vec3()) -> None:
     self.orig = origin
     self.dir = direction
 def __init__(self, origin = Point3(), direction = Vec3()):
     self.o = origin
     self.d = direction
Beispiel #24
0
    def __init__(self, p0: Point3, p1: Point3, mat: Material) -> None:
        self.box_min = p0
        self.box_max = p1

        self.sides = HittableList()
        self.sides.add(XYRect(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), mat))
        self.sides.add(
            FlipFace(XYRect(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), mat)))
        self.sides.add(XZRect(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), mat))
        self.sides.add(
            FlipFace(XZRect(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), mat)))
        self.sides.add(YZRect(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), mat))
        self.sides.add(
            FlipFace(YZRect(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), mat)))
Beispiel #25
0
 def value(self, u: float, v: float, p: Point3) -> Color:
     # return Color(1, 1, 1) * 0.5 * (1 + self.noise.noise(self.scale * p))
     # return Color(1, 1, 1) * self.noise.turb(self.scale * p)
     return (Color(1, 1, 1) * 0.5 *
             (1 + np.sin(self.scale * p.z() + 10 * self.noise.turb(p))))
Beispiel #26
0
 def value(self, u: float, v: float, p: Point3) -> Color:
     sines = np.sin(10 * p.x()) * np.sin(10 * p.y()) * np.sin(10 * p.z())
     if sines < 0:
         return self.odd.value(u, v, p)
     else:
         return self.even.value(u, v, p)
Beispiel #27
0
def final_scene(aspect_ratio: float, time0: float, time1: float) \
        -> Tuple[BVHNode, Camera]:
    world = HittableList()

    # Ground
    boxes1 = HittableList()
    ground = Lambertian(SolidColor(0.48, 0.83, 0.53))
    boxes_per_side = 20
    for i in range(boxes_per_side):
        for j in range(boxes_per_side):
            w = 100
            x0 = -1000 + i * w
            z0 = -1000 + j * w
            y0 = 0
            x1 = x0 + w
            y1 = random_float(1, 101)
            z1 = z0 + w
            boxes1.add(Box(Point3(x0, y0, x0), Point3(x1, y1, z1), ground))
    world.add(BVHNode(boxes1.objects, time0, time1))

    # Light
    light = DiffuseLight(SolidColor(7, 7, 7))
    world.add(XZRect(123, 423, 147, 412, 554, light))

    # Moving sphere
    center1 = Point3(400, 400, 200)
    center2 = center1 + Vec3(30, 0, 0)
    moving_sphere_material = Lambertian(SolidColor(0.7, 0.3, 0.1))
    world.add(MovingSphere(center1, center2, 0, 1, 50, moving_sphere_material))

    # Dielectric & metal balls
    world.add(Sphere(Point3(260, 150, 45), 50, Dielectric(1.5)))
    world.add(
        Sphere(Point3(0, 150, 145), 50, Metal(Color(0.8, 0.8, 0.9), 10.0)))

    # The subsurface reflection sphere
    boundary = Sphere(Point3(360, 150, 145), 70, Dielectric(1.5))
    world.add(boundary)
    world.add(ConstantMedium(boundary, 0.2, SolidColor(0.2, 0.4, 0.9)))

    # Big thin mist
    mist = Sphere(Point3(0, 0, 0), 5000, Dielectric(1.5))
    world.add(ConstantMedium(mist, 0.0001, SolidColor(1, 1, 1)))

    # Earth and marble ball
    emat = Lambertian(ImageTexture("earthmap.jpg"))
    world.add(Sphere(Point3(400, 200, 400), 100, emat))
    pertext = NoiseTexture(0.1)
    world.add(Sphere(Point3(220, 280, 300), 80, Lambertian(pertext)))

    # Foam
    boxes2 = HittableList()
    white = Lambertian(SolidColor(0.73, 0.73, 0.73))
    ns = 1000
    for j in range(ns):
        boxes2.add(Sphere(Point3.random(0, 165), 10, white))
    world.add(
        Translate(RotateY(BVHNode(boxes2.objects, time0, time1), 15),
                  Vec3(-100, 270, 395)))

    world_bvh = BVHNode(world.objects, time0, time1)

    lookfrom = Point3(478, 278, -600)
    lookat = Point3(278, 278, 0)
    vup = Vec3(0, 1, 0)
    vfov = 40
    dist_to_focus: float = 10
    aperture: float = 0
    cam = Camera(lookfrom, lookat, vup, vfov, aspect_ratio, aperture,
                 dist_to_focus, time0, time1)

    return world_bvh, cam