def ray_color(r: RayList, world: HittableList, depth: int) \ -> Tuple[Optional[RayList], Optional[Vec3List], Vec3List]: length = len(r) if not r.direction().e.any(): return None, None, Vec3List.new_zero(length) # Calculate object hits rec_list: HitRecordList = world.hit(r, 0.001, cp.inf) # Useful empty arrays empty_vec3list = Vec3List.new_zero(length) empty_array_float = cp.zeros(length, cp.float32) empty_array_bool = cp.zeros(length, cp.bool) empty_array_int = cp.zeros(length, cp.int32) # Background / Sky unit_direction = r.direction().unit_vector() sky_condition = Vec3List.from_array((unit_direction.length() > 0) & (rec_list.material == 0)) t = (unit_direction.y() + 1) * 0.5 blue_bg = (Vec3List.from_vec3(Color(1, 1, 1), length).mul_ndarray(1 - t) + Vec3List.from_vec3(Color(0.5, 0.7, 1), length).mul_ndarray(t)) result_bg = Vec3List(cp.where(sky_condition.e, blue_bg.e, empty_vec3list.e)) if depth <= 1: return None, None, result_bg # Material scatter calculations materials: Dict[int, Material] = world.get_materials() scattered_list = RayList.new_zero(length) attenuation_list = Vec3List.new_zero(length) for mat_idx in materials: mat_condition = (rec_list.material == mat_idx) mat_condition_3 = Vec3List.from_array(mat_condition) if not mat_condition.any(): continue ray = RayList( Vec3List(cp.where(mat_condition_3.e, r.orig.e, empty_vec3list.e)), Vec3List(cp.where(mat_condition_3.e, r.dir.e, empty_vec3list.e))) rec = HitRecordList( Vec3List( cp.where(mat_condition_3.e, rec_list.p.e, empty_vec3list.e)), cp.where(mat_condition, rec_list.t, empty_array_float), cp.where(mat_condition, rec_list.material, empty_array_int), Vec3List( cp.where(mat_condition_3.e, rec_list.normal.e, empty_vec3list.e)), cp.where(mat_condition, rec_list.front_face, empty_array_bool)) ray, rec, idx_list = compress(ray, rec) scattered, attenuation = materials[mat_idx].scatter(ray, rec) scattered, attenuation = decompress(scattered, attenuation, idx_list, length) scattered_list += scattered attenuation_list += attenuation return scattered_list, attenuation_list, result_bg
def ray_color(r, world, depth): length = len(r) if not r.direction().e.any(): return None, None, Vec3List.new_zero(length) rec_list = world.hit(r, 0.001, cp.inf) empty_vec3list = Vec3List.new_zero(length) empty_array_float = cp.zeros(length, cp.float32) empty_array_bool = cp.zeros(length, cp.bool) empty_array_int = cp.zeros(length, cp.int32) unit_direction = r.direction().unit_vector() sky_condition = Vec3List.from_array((unit_direction.length() > 0) & (rec_list.material == 0)) t = (unit_direction.y() + 1) * 0.5 blue_bg = (Vec3List.from_vec3(Color(1, 1, 1), length).mul_ndarray(1 - t) + Vec3List.from_vec3(Color(0.5, 0.7, 1), length).mul_ndarray(t)) result_bg = Vec3List(cp.where(sky_condition.e, blue_bg.e, empty_vec3list.e)) if depth <= 1: return None, None, result_bg materials = world.get_materials() scattered_list = RayList.new_zero(length) attenuation_list = Vec3List.new_zero(length) for mat_idx in materials: mat_condition = (rec_list.material == mat_idx) mat_condition_3 = Vec3List.from_array(mat_condition) if not mat_condition.any(): continue ray = RayList( Vec3List( cp.where(mat_condition_3.e, r.origin().e, empty_vec3list.e)), Vec3List( cp.where(mat_condition_3.e, r.direction().e, empty_vec3list.e))) rec = HitRecordList( Vec3List( cp.where(mat_condition_3.e, rec_list.p.e, empty_vec3list.e)), cp.where(mat_condition, rec_list.t, empty_array_float), cp.where(mat_condition, rec_list.material, empty_array_int), Vec3List( cp.where(mat_condition_3.e, rec_list.normal.e, empty_vec3list.e)), cp.where(mat_condition, rec_list.front_face, empty_array_bool)) ray, rec, idx_list = compress(ray, rec) scattered, attenuation = materials[mat_idx].scatter(ray, rec) scattered, attenuation = decompress(scattered, attenuation, idx_list, length) scattered_list += scattered attenuation_list += attenuation return scattered_list, attenuation_list, result_bg
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
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
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
def ray_color(r: Ray, world: HittableList, depth: int) -> Color: if depth <= 0: return Color(0, 0, 0) rec: Optional[HitRecord] = world.hit(r, 0.001, np.inf) if rec is not None: scatter_result = rec.material.scatter(r, rec) if scatter_result is not None: scattered, attenuation = scatter_result return attenuation * ray_color(scattered, world, depth - 1) return Color(0, 0, 0) unit_direction: Vec3 = r.direction().unit_vector() t = (unit_direction.y() + 1) * 0.5 return Color(1, 1, 1) * (1 - t) + Color(0.5, 0.7, 1) * t
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
def scatter(self, r_in: RayList, rec: HitRecordList) \ -> Tuple[RayList, Vec3List]: etai_over_etat = cp.where( rec.front_face, 1 / self.ref_idx, self.ref_idx ) unit_direction = r_in.direction().unit_vector() cos_theta = -unit_direction @ rec.normal cos_theta = cp.where(cos_theta > 1, 1, cos_theta) sin_theta = cp.sqrt(1 - cos_theta**2) reflect_prob = self.schlick(cos_theta, etai_over_etat) reflect_condition = ( (etai_over_etat * sin_theta > 1) | (random_float_list(len(r_in)) < reflect_prob) ) # total internal reflection reflected = (unit_direction.mul_ndarray(reflect_condition)).reflect( rec.normal.mul_ndarray(reflect_condition) ) # refraction refracted = (unit_direction.mul_ndarray(~reflect_condition)).refract( rec.normal.mul_ndarray(~reflect_condition), etai_over_etat ) direction = reflected + refracted condition = rec.t > 0 scattered = RayList( rec.p.mul_ndarray(condition), direction.mul_ndarray(condition) ) attenuation = Vec3List.from_array(condition) * Color(1, 1, 1) return scattered, attenuation
def scatter(self, r_in: Ray, rec: HitRecord) \ -> Optional[Tuple[Ray, Color]]: if rec.front_face: etai_over_etat = 1 / self.ref_idx else: etai_over_etat = self.ref_idx unit_direction: Vec3 = r_in.direction().unit_vector() cos_theta: float = min(-unit_direction @ rec.normal, 1) sin_theta: float = np.sqrt(1 - cos_theta**2) reflect_prob: float = self.schlick(cos_theta, etai_over_etat) if etai_over_etat * sin_theta > 1 or random_float() < reflect_prob: # total internal reflection reflected: Vec3 = unit_direction.reflect(rec.normal) scattered = Ray(rec.p, reflected) else: # refraction refracted: Vec3 = unit_direction.refract( rec.normal, etai_over_etat ) scattered = Ray(rec.p, refracted) attenuation = Color(1, 1, 1) return scattered, attenuation
def main() -> None: aspect_ratio = 1 image_width = 256 image_height = int(image_width / aspect_ratio) samples_per_pixel = 20 max_depth = 10 time0 = 0 time1 = 1 world, cam = scenes.final_scene(aspect_ratio, time0, time1) background = Color(0, 0, 0) 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, background, 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)
def __init__(self, r: Union[float, Color], g: float = None, b: float = None) -> None: if isinstance(r, Color): self.color_value = r elif g is not None and b is not None: self.color_value = Color(r, g, b) else: raise ValueError
def value(self, u: float, v: float, p: Vec3) -> Color: u = np.clip(u, 0, 1) v = 1 - np.clip(v, 0, 1) i = int(u * self.width) j = int(v * self.height) if i >= self.width: i = self.width - 1 if j >= self.height: j = self.height - 1 pixel = self.data[j][i] / 255 return Color(*pixel)
def scan_line(j: int, background: Color, world: BVHNode, cam: Camera, image_width: int, image_height: int, samples_per_pixel: int, max_depth: int) -> Img: img = Img(image_width, 1) for i in range(image_width): pixel_color = Color(0, 0, 0) for s in range(samples_per_pixel): u: float = (i + random_float()) / (image_width - 1) v: float = (j + random_float()) / (image_height - 1) r: Ray = cam.get_ray(u, v) pixel_color += ray_color(r, background, world, max_depth) img.write_pixel(i, 0, pixel_color, samples_per_pixel) print(f"Scanlines remaining: {j} ", end="\r") return img
def scan_line(j: int, world: HittableList, cam: Camera, image_width: int, image_height: int, samples_per_pixel: int, max_depth: int) -> Img: img = Img(image_width, 1) row_pixel_color = Vec3List.from_vec3(Color(), image_width) for s in range(samples_per_pixel): u: np.ndarray = (random_float_list(image_width) + np.arange(image_width)) / (image_width - 1) v: np.ndarray = (random_float_list(image_width) + j) / (image_height - 1) r: RayList = cam.get_ray(u, v) row_pixel_color += ray_color(r, world, max_depth) img.write_pixel_list(0, row_pixel_color, samples_per_pixel) return img
def ray_color(r: Ray, background: Color, world: BVHNode, depth: int) -> Color: # Bounce limit if depth <= 0: return Color(0, 0, 0) rec: Optional[HitRecord] = world.hit(r, 0.001, np.inf) # Ray hits nothing if rec is None: return background emitted = rec.material.emitted(rec.u, rec.v, rec.p) scatter_result = rec.material.scatter(r, rec) # No scattered ray (could be emissive material) if scatter_result is None: return emitted scattered, attenuation = scatter_result return (emitted + ( attenuation * ray_color(scattered, background, world, depth-1) ))
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
def emitted(self, u: float, v: float, p: Point3) -> Color: return Color(0, 0, 0)
def ray_color(r: RayList, world: HittableList, depth: int) -> Vec3List: length = len(r) if not r.direction().e.any(): return Vec3List.new_zero(length) # Calculate object hits rec_list: HitRecordList = world.hit(r, 0.001, np.inf) # Useful empty arrays empty_vec3list = Vec3List.new_zero(length) empty_array_float = np.zeros(length, np.float32) empty_array_bool = np.zeros(length, np.bool) empty_array_int = np.zeros(length, np.int32) # Background / Sky unit_direction = r.direction().unit_vector() sky_condition = Vec3List.from_array((unit_direction.length() > 0) & (rec_list.material == 0)) t = (unit_direction.y() + 1) * 0.5 blue_bg = (Vec3List.from_vec3(Color(1, 1, 1), length).mul_ndarray(1 - t) + Vec3List.from_vec3(Color(0.5, 0.7, 1), length).mul_ndarray(t)) result_bg = Vec3List(np.where(sky_condition.e, blue_bg.e, empty_vec3list.e)) if depth <= 1: return result_bg # Per-material preparations materials: Dict[int, Material] = world.get_materials() material_dict: Dict[int, Tuple[RayList, HitRecordList]] = dict() for mat_idx in materials: mat_condition = (rec_list.material == mat_idx) mat_condition_3 = Vec3List.from_array(mat_condition) if not mat_condition.any(): continue raylist_temp = RayList( Vec3List(np.where(mat_condition_3.e, r.orig.e, empty_vec3list.e)), Vec3List(np.where(mat_condition_3.e, r.dir.e, empty_vec3list.e))) reclist_temp = HitRecordList( Vec3List( np.where(mat_condition_3.e, rec_list.p.e, empty_vec3list.e)), np.where(mat_condition, rec_list.t, empty_array_float), np.where(mat_condition, rec_list.material, empty_array_int), Vec3List( np.where(mat_condition_3.e, rec_list.normal.e, empty_vec3list.e)), np.where(mat_condition, rec_list.front_face, empty_array_bool)) material_dict[mat_idx] = raylist_temp, reclist_temp # Material scatter calculations scattered_list = RayList.new_zero(length) attenuation_list = Vec3List.new_zero(length) for key in material_dict: ray, rec = material_dict[key] ray, rec, idx_list = compress(ray, rec) scattered, attenuation = materials[key].scatter(ray, rec) scattered, attenuation = decompress(scattered, attenuation, idx_list, length) scattered_list += scattered attenuation_list += attenuation result_hittable = (attenuation_list * ray_color(scattered_list, world, depth - 1)) return result_hittable + result_bg
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))))
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