def bounding_box_on_time(self, time: float) -> AABB: box = AABB( self.get_center(time) - Vector3(self._radius, self._radius, self._radius), self.get_center(time) + Vector3(self._radius, self._radius, self._radius)) return box
class HitRecord(object): t: float = 0.0 hit_point: Vector3 = Vector3(0.0, 0.0, 0.0) hit_point_normal: Vector3 = Vector3(0.0, 0.0, 0.0) u: float = 0.0 v: float = 0.0 material: Material = None
def calculate_ray_color(self, ray: Ray, depth: float): has_hit, hit_record = self._bvh_world.hit(ray=ray, t_min=0.001, t_max=float('inf')) if has_hit is True: scattered, attenuation = hit_record.material.scatter( ray_incident=ray, hit_record=hit_record) emitted = hit_record.material.emitted(u=hit_record.u, v=hit_record.v, p=hit_record.hit_point) if depth < 50 and scattered is not None: return emitted + attenuation * self.calculate_ray_color( ray=scattered, depth=depth + 1) else: return emitted else: # Ambient Color # return Vector3(0.0, 0.0, 0.0) ray_direction = Vector3.normalize(ray.direction) t = 0.5 * (ray_direction.y() + 1.0) return (1.0 - t) * Vector3(0.2, 0.2, 0.2) + t * Vector3( 0.5, 0.7, 1.0)
def _render_patch(self, patch_number: int, begin_row: int, begin_column: int, patch_width: int, patch_height: int): """Render a patch of the image defined by {row} {column} with {width} and {height}""" logging.info( f'Beginning patch {patch_number}: ({begin_row} {begin_column}) with width {patch_width} and height {patch_height}' ) max_color_value = 255.99 result = list() for row in range(begin_row, begin_row - patch_height, -1): for column in range(begin_column, begin_column + patch_width): final_color = Vector3() for s in range(self.num_samples): u = float(column + random.random()) / float(self.width) v = float(row + random.random()) / float(self.height) r = self.camera.get_ray(u=u, v=v) final_color = final_color + self.calculate_ray_color( ray=r, depth=0) final_color = final_color / self.num_samples # make the sqrt for the gamma correction final_color = Vector3( math.sqrt(final_color.r()) * max_color_value, math.sqrt(final_color.g()) * max_color_value, math.sqrt(final_color.b()) * max_color_value) result.append((row, column, final_color)) logging.info( f'Ending patch {patch_number} ({begin_row} {begin_column}) with width {patch_width} and height {patch_height}' ) return result
def random_in_unit_sphere(): p = Vector3(1.0, 1.0, 1.0) while p.length() >= 1.0: p = 2.0 * Vector3(random.random(), random.random(), random.random()) - Vector3(1, 1, 1) return p
def value(self, u: float, v: float, p: Vector3) -> Vector3: sines = math.sin(10 * p.x()) * math.sin(10 * p.y()) * math.sin( 10 * p.z()) if sines < 0: return self._texture0.value(u, v, p) else: return self._texture1.value(u, v, p)
def perlin_generate(self): p = list() for i in range(256): p.append( Vector3.normalize( Vector3(-1 + 2 * random.random(), -1 + 2 * random.random(), -1 + 2 * random.random()))) return p
def first_light_scene(): list_of_hitables = [ Sphere(center=Vector3(0.0, -1000, 0.0), radius=1000, material=Lambertian(albedo=Vector3(0.9, 0.67, 0.25))), Sphere(center=Vector3(0.0, 2, 0.0), radius=2, material=Lambertian(albedo=Vector3(0.7, 0.8, 0.1))), Sphere(center=Vector3(0.0, 7, 0.0), radius=2, material=DiffuseLight(albedo=Vector3(1.0, 1.0, 1.0), texture=ConstantTexture(color=Vector3(4,4,4)))), XYRect(3, 5, 1, 3, -2, DiffuseLight(albedo=Vector3(1.0, 1.0, 1.0), texture=ConstantTexture(color=Vector3(1, 1, 1)))) ] return list_of_hitables
def __init__(self, albedo: Vector3 = Vector3(1.0, 1.0, 1.0), texture: Texture = None): """ Lamberian Material :param albedo: albedo color of the material :type albedo: Vector3 """ super().__init__() self._albedo = albedo self._texture = ConstantTexture(Vector3( 1.0, 1.0, 1.0)) if texture is None else texture
def surrounding_box(box0, box1): # try to use annotations smallest = Vector3( min(box0.min.x(), box1.min.x()), min(box0.min.y(), box1.min.y()), min(box0.min.z(), box1.min.z()), ) biggest = Vector3( max(box0.max.x(), box1.max.x()), max(box0.max.y(), box1.max.y()), max(box0.max.z(), box1.max.z()), ) return AABB(min=smallest, max=biggest)
def trilineart_interp(self, c, u, v, w): uu = u * u * (3 - 2 * u) vv = v * v * (3 - 2 * v) ww = w * w * (3 - 2 * w) accum = 0 for i in range(2): for j in range(2): for k in range(2): weight_v = Vector3(u - i, v - j, w - k) accum += (i * uu + (1 - i) * (1 - uu)) * \ (j * vv + (1 - j) * (1 - vv)) * \ (k * ww + (1 - k) * (1 - ww)) * Vector3.dot(c[i * 2 + j * 2 + k], weight_v) return accum
def hit(self, ray: Ray, t_min: float, t_max: float) -> (bool, HitRecord): """Checks if the ray 'ray' hits with the plane between t_min and t_max. Returns true if there is a collision, false otherwise. Return the hitRecord information if the collision is true. """ if ray.direction.y() == 0.0: return False, None t = (self._k - ray.origin.y()) / ray.direction.y() if t < t_min or t > t_max: return False, None x = ray.origin.x() + t * ray.direction.x() z = ray.origin.z() + t * ray.direction.z() if x < self._x0 or x > self._x1 or z < self._z0 or z > self._z1: return False, None record = HitRecord() record.t = t record.hit_point = ray.point_at_parameter(record.t) record.hit_point_normal = Vector3(0, 1, 1) record.u = (x - self._x0) / (self._x1 - self._x0) record.v = (z - self._z0) / (self._z1 - self._z0) record.material = self._material return True, record
def random_cosine_direct(): r1, r2 = random.random(), random.random() z = math.sqrt(1 - r2) phi = 2 * math.pi * r1 x = math.cos(phi) * 2 * math.sqrt(r2) y = math.sin(phi) * 2 * math.sqrt(r2) return Vector3(x, y, z)
def get_sphere_uv(self, p: Vector3) -> (float, float): """Get the uv coordinates of a point in the sphere""" d = Vector3.normalize(p - self._center) phi = math.atan2(d.z(), d.x()) / (2 * math.pi) u = phi + 0.5 v = 0.5 - (math.asin(d.y()) / math.pi) return u, v
def __init__(self, width, height, color_matrix=None): self.width = width self.height = height if color_matrix is None: self.color_matrix = list() for row in range(self.height): self.color_matrix.append([Vector3(0.0, 0.0, 0.0)] * self.width) else: self.color_matrix = color_matrix
def __init__(self, lookfrom:'Vector3', lookat:'Vector3', vectorup:'Vector3', vfov:'float', aspect:'float', aperture:'float', focus_dist:'float', t0:float, t1:float): theta = vfov * math.pi / 180 # vfov to radians half_height = math.tan(theta / 2) # h = tan(theta / 2) half_width = half_height * aspect self._lens_radius = aperture / 2 self._time0 = t0 self._time1 = t1 self._camera_origin = lookfrom w = Vector3.normalize(lookfrom - lookat) u = Vector3.normalize(Vector3.cross(vectorup, w)) v = Vector3.cross(w, u) self._lower_left_corner = self._camera_origin - half_width * focus_dist * u - half_height * focus_dist * v - focus_dist*w self._horizontal = 2 * half_width * focus_dist * u self._vertical = 2 * half_height * focus_dist * v
def random_to_sphere(self, radius: float, distance_squared: float) -> Vector3: r1 = random.random() r2 = random.random() z = 1 + r2 * ( math.sqrt(1 - self._radius * self._radius / distance_squared) - 1) phi = 2 * math.pi * r1 x = math.cos(phi) * math.sqrt(1 - z * z) y = math.sin(phi) * math.sqrt(1 - z * z) return Vector3(x, y, z)
def noise(self, p: Vector3): u = p.x() - math.floor(p.x()) v = p.y() - math.floor(p.y()) w = p.z() - math.floor(p.z()) i = math.floor(p.x()) j = math.floor(p.y()) k = math.floor(p.z()) # return self._ranvector[self._perm_x[i] ^ self._perm_y[j] ^ self._perm_z[k]] # c = [[[self._ranfloat[self._perm_x[(i + ii) & 255] ^ self._perm_y[(j + jj) & 255] ^ self._perm_z[(k + kk) & 255]] for kk in range(2)] for jj in range(2)] for ii in range(2)] c = list() for ii in range(2): for jj in range(2): for kk in range(2): c.append(self._ranvector[self._perm_x[(i + ii) & 255] ^ self._perm_y[(j + jj) & 255] ^ self._perm_z[(k + kk) & 255]]) return self.trilineart_interp(c, u, v, w)
def schlick(self, incident: 'Vector3', normal: 'Vector3', n1: 'float', n2: 'float'): r0 = (n1 - n2) / (n1 + n2) r0 *= r0 cosX = Vector3.dot(normal, incident) if n1 > n2: n = n1 / n2 sinT2 = n * n * (1.0 - cosX * cosX) if sinT2 > 1.0: return 1.0 cosX = math.sqrt(1.0 - sinT2) x = 1.0 - cosX return r0 + (1.0 - r0) * math.pow(x, 5)
def value(self, u: float, v: float, p: Vector3) -> Vector3: i = (self.uoffset - u) * (self._width - 1) j = (self.voffset + v) * (self._height - 1) i = i % self._width j = j % self._height # i = MathUtils.clamp(i, 0, self._width - 1) # j = MathUtils.clamp(j, 0, self._height - 1) pix = self._image.load() r = int(pix[i, j][0]) / 255.0 g = int(pix[i, j][1]) / 255.0 b = int(pix[i, j][2]) / 255.0 return Vector3(r, g, b)
def build_from_w(self, n: Vector3): self._w = Vector3.normalize(n) if math.fabs(self._w.x()) > 0.9: a = Vector3(0, 1, 0) else: a = Vector3(1, 0, 0) self._v = Vector3.normalize(Vector3.cross(self._w, a)) self._u = Vector3.cross(self._w, self._v)
def hit(self, ray: Ray, t_min: float, t_max: float) -> (bool, HitRecord): """Checks if the ray 'ray' hits with the sphere between t_min and t_max. Returns true if there is a collision, false otherwise. Return the hitRecord information if the collision is true. """ current_center = self.get_center(t=ray.time) oc = ray.origin - current_center a = Vector3.dot(ray.direction, ray.direction) b = Vector3.dot(oc, ray.direction) c = Vector3.dot(oc, oc) - self._radius * self._radius discriminant = b * b - a * c if discriminant > 0: temp = (-b - math.sqrt(b * b - a * c)) / a if t_max > temp > t_min: record = HitRecord() record.t = temp record.hit_point = ray.point_at_parameter(record.t) record.hit_point_normal = (record.hit_point - current_center) / self._radius record.u, record.v = self.get_sphere_uv(p=record.hit_point) record.material = self._material return True, record temp = (-b + math.sqrt(b * b - a * c)) / a if t_max > temp > t_min: record = HitRecord() record.t = temp record.hit_point = ray.point_at_parameter(record.t) record.hit_point_normal = (record.hit_point - current_center) / self._radius record.u, record.v = self.get_sphere_uv(p=record.hit_point) record.material = self._material return True, record return False, None
def hit(self, ray: Ray, t_min: float, t_max: float) -> (bool, HitRecord): """Checks if the ray 'ray' hits with any of the walls between t_min and t_max.""" # db = False has_hit, rec1 = self._boundary.hit(ray=ray, t_min=-float('inf'), t_max=float('inf')) if has_hit is True: has_hit_again, rec2 = self._boundary.hit(ray=ray, t_min=rec1.t + 0.0001, t_max=float('inf')) if has_hit_again is True: if rec1.t < t_min: rec1.t = t_min if rec2.t > t_max: rec2.t = t_max # if db: # print(f't0 t1 {rec1.t} {rec2.t}') if rec1.t >= rec2.t: return False, None if rec1.t < 0: rec1.t = 0 distance_inside_boundary = (rec2.t - rec1.t) * ray.direction.length() hit_distance = -(1 / self._density) * math.log( random.random()) # base 10 ? # if db: # print(f'DATA: {hit_distance} {distance_inside_boundary} {hit_distance < distance_inside_boundary}') if hit_distance < distance_inside_boundary: # if db: # print(f'hit distance = {hit_distance}') hit_record = HitRecord() hit_record.t = rec1.t + hit_distance / ray.direction.length( ) # if db: # print(f'rec.t = {hit_record.t}') hit_record.hit_point = ray.point_at_parameter(hit_record.t) # if db: # print(f'rec.hit_point = {hit_record.hit_point}') hit_record.hit_point_normal = Vector3(1, 0, 0) hit_record.material = self._phase_function return True, hit_record return False, None
def scatter(self, ray_incident: Ray, hit_record: HitRecord): reflected_vector = Vector3.reflect( Vector3.normalize(ray_incident.direction), hit_record.hit_point_normal) attenuation = Vector3(1.0, 1.0, 1.0) refracted_vector = Vector3.refract(incident=ray_incident.direction, normal=hit_record.hit_point_normal, n1=1.0, n2=self._refraction_index) reflect_probability = self.schlick(incident=ray_incident.direction, normal=hit_record.hit_point_normal, n1=1.0, n2=self._refraction_index) if random.random() < reflect_probability: scattered = Ray(hit_record.hit_point, reflected_vector, ray_incident.time) else: scattered = Ray(hit_record.hit_point, refracted_vector, ray_incident.time) return scattered, attenuation
def one_sphere_noise() -> (list, Camera): lookfrom = Vector3(13, 2, 3) lookat = Vector3(0, 0, 0) camera = Camera(lookfrom=lookfrom, lookat=lookat, vectorup=Vector3(0.0, 1.0, 0.0), vfov=20.0, aspect=16 / 9, aperture=0.0, focus_dist=10.0, t0=0.0, t1=1.0) pertext = NoiseTexture(scale=1.0) list_of_hitables = [ Sphere(Vector3(0, -1000, 0), 1000, Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=pertext)), Sphere(Vector3(0, 2, 0), 2, Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=pertext)) ] return list_of_hitables, camera
def one_sphere_world(): lookfrom = Vector3(12, 5, 3) lookat = Vector3(0, 2, -1) camera = Camera(lookfrom=lookfrom, lookat=lookat, vectorup=Vector3(0.0, 1.0, 0.0), vfov=20.0, aspect=16 / 9, aperture=0.0, focus_dist=10.0, t0=0.0, t1=1.0) checker_texture = CheckerTexture(texture0=ConstantTexture(Vector3(0.2, 0.3, 0.1)), texture1=ConstantTexture(Vector3(0.9, 0.9, 0.9))) script_dir = os.path.dirname(__file__) texture_filepath = os.path.join(script_dir, r'../resources/textures/google-maps.jpg') world_texture = ImageTexture(texture_file_path=texture_filepath, uoffset=0.1) texture_filepath = os.path.join(script_dir, r'../resources/textures/tiled-background-with-stripes-and-splatter_256x256.jpg') colors_texture = ImageTexture(texture_file_path=texture_filepath) list_of_hitables = [ # Sphere(center=Vector3(0.0, -1000, 0.0), radius=1000, # material=Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=colors_texture)), XZRect(-20, 20, -20, 20, 0, material=Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=colors_texture)), Sphere(center=Vector3(0, 2, 0), radius=2.0, material=Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=world_texture)), Sphere(center=Vector3(0, 1.0, -3), radius=1.0,material=Lambertian(albedo=Vector3(1.0, 1.0, 1.0), texture=checker_texture)) ] return list_of_hitables, camera
def random_in_unit_disk(self): p = Vector3(1.0, 1.0, 0.0) while p.length() >= 1.0: p = 2.0 * Vector3(random.random(), random.random(), 0) - Vector3(1, 1, 0) return p
def bounding_box(self, t0: float, t1: float) -> AABB: return AABB(min=Vector3(self._x0, self._y0, self._k - 0.0001), max=Vector3(self._x1, self._y1, self._k + 0.0001))
def value(self, direction: Vector3) -> float: cosine = Vector3.dot(Vector3.normalize(direction), self._uvw.w()) if cosine > 0: return cosine / math.pi else: return 0
def emitted(self, u: float, v: float, p: Vector3) -> Vector3: return Vector3(0.0, 0.0, 0.0)