def testHit(self): sphere = Sphere() ray1 = Ray(origin=Point(0, 0, 2), dir=-VEC_Z) intersection1 = sphere.ray_intersection(ray1) assert intersection1 assert HitRecord( world_point=Point(0.0, 0.0, 1.0), normal=Normal(0.0, 0.0, 1.0), surface_point=Vec2d(0.0, 0.0), t=1.0, ray=ray1, material=sphere.material, ).is_close(intersection1) ray2 = Ray(origin=Point(3, 0, 0), dir=-VEC_X) intersection2 = sphere.ray_intersection(ray2) assert intersection2 assert HitRecord( world_point=Point(1.0, 0.0, 0.0), normal=Normal(1.0, 0.0, 0.0), surface_point=Vec2d(0.0, 0.5), t=2.0, ray=ray2, material=sphere.material, ).is_close(intersection2) assert not sphere.ray_intersection( Ray(origin=Point(0, 10, 2), dir=-VEC_Z))
def testTransformation(self): sphere = Sphere(transformation=translation(Vec(10.0, 0.0, 0.0))) ray1 = Ray(origin=Point(10, 0, 2), dir=-VEC_Z) intersection1 = sphere.ray_intersection(ray1) assert intersection1 assert HitRecord( world_point=Point(10.0, 0.0, 1.0), normal=Normal(0.0, 0.0, 1.0), surface_point=Vec2d(0.0, 0.0), t=1.0, ray=ray1, material=sphere.material, ).is_close(intersection1) ray2 = Ray(origin=Point(13, 0, 0), dir=-VEC_X) intersection2 = sphere.ray_intersection(ray2) assert intersection2 assert HitRecord( world_point=Point(11.0, 0.0, 0.0), normal=Normal(1.0, 0.0, 0.0), surface_point=Vec2d(0.0, 0.5), t=2.0, ray=ray2, material=sphere.material, ).is_close(intersection2) # Check if the sphere failed to move by trying to hit the untransformed shape assert not sphere.ray_intersection( Ray(origin=Point(0, 0, 2), dir=-VEC_Z)) # Check if the *inverse* transformation was wrongly applied assert not sphere.ray_intersection( Ray(origin=Point(-10, 0, 0), dir=-VEC_Z))
def test_vec_point_multiplication(self): m = Transformation( m=[ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 9.0, 8.0, 7.0], [0.0, 0.0, 0.0, 1.0], ], invm=[ [-3.75, 2.75, -1, 0], [5.75, -4.75, 2.0, 1.0], [-2.25, 2.25, -1.0, -2.0], [0.0, 0.0, 0.0, 1.0], ], ) assert m.is_consistent() expected_v = Vec(14.0, 38.0, 51.0) assert expected_v.is_close(m * Vec(1.0, 2.0, 3.0)) expected_p = Point(18.0, 46.0, 58.0) assert expected_p.is_close(m * Point(1.0, 2.0, 3.0)) expected_n = Normal(-8.75, 7.75, -3.0) assert expected_n.is_close(m * Normal(3.0, 2.0, 4.0))
def testTransformation(self): plane = Plane(transformation=rotation_y(angle_deg=90.0)) ray1 = Ray(origin=Point(1, 0, 0), dir=-VEC_X) intersection1 = plane.ray_intersection(ray1) assert intersection1 assert HitRecord( world_point=Point(0.0, 0.0, 0.0), normal=Normal(1.0, 0.0, 0.0), surface_point=Vec2d(0.0, 0.0), t=1.0, ray=ray1, material=plane.material, ).is_close(intersection1) ray2 = Ray(origin=Point(0, 0, 1), dir=VEC_Z) intersection2 = plane.ray_intersection(ray2) assert not intersection2 ray3 = Ray(origin=Point(0, 0, 1), dir=VEC_X) intersection3 = plane.ray_intersection(ray3) assert not intersection3 ray4 = Ray(origin=Point(0, 0, 1), dir=VEC_Y) intersection4 = plane.ray_intersection(ray4) assert not intersection4
def ray_intersection(self, ray: Ray) -> Union[HitRecord, None]: """Checks if a ray intersects the plane Return a `HitRecord`, or `None` if no intersection was found. """ inv_ray = ray.transform(self.transformation.inverse()) if abs(inv_ray.dir.z) < 1e-5: return None t = -inv_ray.origin.z / inv_ray.dir.z if (t <= inv_ray.tmin) or (t >= inv_ray.tmax): return None hit_point = inv_ray.at(t) return HitRecord( world_point=self.transformation * hit_point, normal=self.transformation * Normal(0.0, 0.0, 1.0 if inv_ray.dir.z < 0.0 else -1.0), surface_point=Vec2d(hit_point.x - floor(hit_point.x), hit_point.y - floor(hit_point.y)), t=t, ray=ray, material=self.material, )
def scatter_ray(self, pcg: PCG, incoming_dir: Vec, interaction_point: Point, normal: Normal, depth: int): # There is no need to use the PCG here, as the reflected direction is always completely deterministic # for a perfect mirror ray_dir = Vec(incoming_dir.x, incoming_dir.y, incoming_dir.z).normalize() normal = normal.to_vec().normalize() dot_prod = normal.dot(ray_dir) return Ray( origin=interaction_point, dir=ray_dir - normal * 2 * dot_prod, tmin=1e-5, tmax=inf, depth=depth, )
def testNormals(self): sphere = Sphere(transformation=scaling(Vec(2.0, 1.0, 1.0))) ray = Ray(origin=Point(1.0, 1.0, 0.0), dir=Vec(-1.0, -1.0)) intersection = sphere.ray_intersection(ray) # We normalize "intersection.normal", as we are not interested in its length assert intersection.normal.normalize().is_close( Normal(1.0, 4.0, 0.0).normalize())
def testNormalDirection(self): # Scaling a sphere by -1 keeps the sphere the same but reverses its # reference frame sphere = Sphere(transformation=scaling(Vec(-1.0, -1.0, -1.0))) ray = Ray(origin=Point(0.0, 2.0, 0.0), dir=-VEC_Y) intersection = sphere.ray_intersection(ray) # We normalize "intersection.normal", as we are not interested in its length assert intersection.normal.normalize().is_close( Normal(0.0, 1.0, 0.0).normalize())
def _sphere_normal(point: Point, ray_dir: Vec) -> Normal: """Compute the normal of a unit sphere The normal is computed for `point` (a point on the surface of the sphere), and it is chosen so that it is always in the opposite direction with respect to `ray_dir`. """ result = Normal(point.x, point.y, point.z) return result if (point.to_vec().dot(ray_dir) < 0.0) else -result
def testInnerHit(self): sphere = Sphere() ray = Ray(origin=Point(0, 0, 0), dir=VEC_X) intersection = sphere.ray_intersection(ray) assert intersection assert HitRecord( world_point=Point(1.0, 0.0, 0.0), normal=Normal(-1.0, 0.0, 0.0), surface_point=Vec2d(0.0, 0.5), t=1.0, ray=ray, ).is_close(intersection)
def __mul__(self, other): if isinstance(other, Vec): row0, row1, row2, row3 = self.m return Vec( x=other.x * row0[0] + other.y * row0[1] + other.z * row0[2], y=other.x * row1[0] + other.y * row1[1] + other.z * row1[2], z=other.x * row2[0] + other.y * row2[1] + other.z * row2[2]) elif isinstance(other, Point): row0, row1, row2, row3 = self.m p = Point(x=other.x * row0[0] + other.y * row0[1] + other.z * row0[2] + row0[3], y=other.x * row1[0] + other.y * row1[1] + other.z * row1[2] + row1[3], z=other.x * row2[0] + other.y * row2[1] + other.z * row2[2] + row2[3]) w = other.x * row3[0] + other.y * row3[1] + other.z * row3[ 2] + row3[3] if w == 1.0: return p else: return Point(p.x / w, p.y / w, p.z / w) elif isinstance(other, Normal): row0, row1, row2, _ = self.invm return Normal( x=other.x * row0[0] + other.y * row1[0] + other.z * row2[0], y=other.x * row0[1] + other.y * row1[1] + other.z * row2[1], z=other.x * row0[2] + other.y * row1[2] + other.z * row2[2]) elif isinstance(other, Transformation): result_m = _matr_prod(self.m, other.m) result_invm = _matr_prod( other.invm, self.invm) # Reverse order! (A B)^-1 = B^-1 A^-1 return Transformation(m=result_m, invm=result_invm) else: raise TypeError( f"Invalid type {type(other)} multiplied to a Transformation object" )