class Vec3 def initialize(x = 0.0, y = 0.0, z = 0.0) @x = x @y = y @z = z end attr_reader :x, :y, :z def +(oth) self.class.new(@x + oth.x, @y + oth.y, @z + oth.z) end def -@ self.class.new(-@x, -@y, -@z) end def -(oth) self + -oth end def *(oth) if oth.is_a?(Vec3) self.class.new(@x * oth.x, @y * oth.y, @z * oth.z) else self.class.new(@x * oth, @y * oth, @z * oth) end end def /(oth) self * (1.0 / oth) end def dot(oth) v = self * oth v.x + v.y + v.z end def cross(oth) self.class.new( @y * oth.z - @z * oth.y, @z * oth.x - @x * oth.z, @x * oth.y - @y * oth.x ) end def mag_sqr @x ** 2 + @y ** 2 + @z ** 2 end def mag mag_sqr ** 0.5 end def unit self / mag end def reflect(normal) self - normal * dot(normal) * 2 end def refract(normal, etaratio) costheta = [(-self).dot(normal), 1.0].min rout_perp = (self + normal * costheta) * etaratio rout_parr = normal * -((1.0 - rout_perp.mag_sqr).abs ** 0.5) rout_perp + rout_parr end def in_unit? mag_sqr < 1 end def near_zero?(s = 1e-8) @x.abs < s && @y.abs < s && @z.abs < s end def to_s "{#{@x}, #{@y}, #{@z}}" end def self.random(min: -1.0, max: 1.0, dimensions: 3) interval = Interval.new(min, max) self.new( dimensions >= 1 ? interval.sample : 0, dimensions >= 2 ? interval.sample : 0, dimensions >= 3 ? interval.sample : 0 ) end def self.random_in_unit(normal: nil, dimensions: 3) p = nil while p.nil? c = self.random(dimensions: dimensions) p = c.unit if c.in_unit? end normal.nil? || p.dot(normal) > 0 ? p : -p end end class Point < Vec3; end class Colour < Vec3 def r; @x; end def g; @y; end def b; @z; end def gamma(gma = 2) Colour.new( r > 0 ? r ** (1.0 / gma) : r, g > 0 ? g ** (1.0 / gma) : g, b > 0 ? b ** (1.0 / gma) : b ) end def clamp(interval) Colour.new( interval.clamp(r), interval.clamp(g), interval.clamp(b) ) end def to_ppm g_space = gamma.clamp(Interval.new(0.0, 0.999)) "%3d %3d %3d" % [ (g_space.r * 256).to_i, (g_space.g * 256).to_i, (g_space.b * 256).to_i ] end end