def hit(self, ray: Ray, t_min: float, t_max: float) -> Optional[HitRecord]: origin_to_center: Vec3 = ray.origin - self.center # The following are just "components" of the quadratic formula, derived # from vectors and with some redundant 2's canceled out to begin with. a: float = ray.direction.dot(ray.direction) b: float = origin_to_center.dot(ray.direction) c: float = origin_to_center.dot(origin_to_center) - (self.radius**2) discriminant: float = (b**2) - (a * c) if discriminant > 0: neg_conjugate: float = (-b - math.sqrt(discriminant)) / a # The original C++ code, again, saves on stack space by re-using a # generic variable `temp`. But heh, I figured can't I skimp a bit on # the manual optimization and leave that up to the interpeter? pos_conjugate: float = (-b + math.sqrt(discriminant)) / a # Also this code was "refactored" from the original C++ to read more # Pythonic. chosen_conjugate = self.__decide_conjugate(t_min, t_max, neg_conjugate, pos_conjugate) if chosen_conjugate is not None: t = chosen_conjugate p = ray.point_at_parameter(t) normal = (p - self.center) / self.radius return HitRecord(t, p, normal, self.material) return None
def color(ray: Ray) -> Vec3: """ Linear interpolation of color based on the y direction. As for hitting the sphere, note that the brightness of an object with respect to its light source is dependent on its normal vectors. The greater the angle between the normal and the light ray, the darker that spot is. Obvious implication: the sphere is brightest where the angle between the normal and the light ray is 0---that is, when the light ray is parallel to the normal. """ t: float = hit_sphere(Vec3(0, 0, -1), 0.5, ray) if t > 0: normal: Vec3 = (ray.point_at_parameter(t) - Vec3(0, 0, -1)).unit_vector() # For all I can tell, what this does is to "scale" the normal such that # (a) there are no negatives and (b) it will not all devolve to just 0, # which leaves us with just a black circle. Multiplying by .5 ensures # that the resulting sphere isn't too dark or too light. # # - Without the scaling factor (0.5), the image is just too bright. # - Without the increments to each component of the normal, the # resulting image will tend to have negative values, which are invalid # in the PPM spec to begin with, and just plain doesn't make sense. return 0.5 * Vec3(normal.x + 1, normal.y + 1, normal.z + 1) unit_direction: Vec3 = ray.direction.unit_vector() # Note that t's definition was pulled up. I guess he was just saving on some # stack space. t = 0.5 * (unit_direction.y + 1) return (UNIT_VEC3 * (1.0 - t)) + (Vec3(0.5, 0.7, 1.0) * t)