class HitRecord def initialize(point, t, ray, out_normal, material) @point = point @t = t @front_face = ray.direction.dot(out_normal) < 0 @normal = @front_face ? out_normal : -out_normal @material = material end attr_reader :point, :normal, :t, :front_face, :material end class Hittable def hit(ray, trange) nil end end class Hittables < Hittable def initialize clear end def clear @objects = [] end def <<(object) @objects << object end def hit(ray, trange) rec = nil closest = trange.max @objects.each do |object| if trec = object.hit(ray, Interval.new(trange.min, closest)) rec = trec closest = trec.t end end rec end end class Sphere < Hittable def initialize(centre, radius = 1, material) @centre = centre @radius = radius @material = material end attr_reader :centre, :radius def hit(ray, trange) oc = @centre - ray.origin a = ray.direction.mag_sqr h = ray.direction.dot(oc) c = oc.mag_sqr - @radius ** 2 disc = h ** 2 - a * c return nil if disc < 0 sqrtd = disc ** 0.5 root = (h - sqrtd) / a if !trange.surround?(root) root = (h + sqrtd) / a if !trange.surround?(root) return nil end end t = root p = ray.at(t) o_n = (p - @centre) / @radius HitRecord.new(p, t, ray, o_n, @material) end end