aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNat Lasseter <user@4574.co.uk>2024-06-12 17:37:40 +0100
committerNat Lasseter <user@4574.co.uk>2024-06-12 17:37:40 +0100
commitdf7c4a808d691e1d927047bb86aa9738780e694d (patch)
treeda197b87ebf2c48fe27b9aabf2db1fff5786c7f4
Initial Commit (Up to Chapter 7)
-rw-r--r--.gitignore2
-rw-r--r--Makefile10
-rw-r--r--Readme6
-rw-r--r--lib/hittable.rb81
-rw-r--r--lib/interval.rb20
-rw-r--r--lib/ray.rb12
-rw-r--r--lib/vec3.rb74
-rwxr-xr-xrtiaw79
8 files changed, 284 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..470cb69
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.swp
+out.ppm
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3536afe
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+out.ppm: rtiaw $(wildcard lib/*.rb)
+ ./$< > $@
+
+.phony: view clean
+
+view: out.ppm
+ eog out.ppm &
+
+clean:
+ rm -f out.ppm
diff --git a/Readme b/Readme
new file mode 100644
index 0000000..28343de
--- /dev/null
+++ b/Readme
@@ -0,0 +1,6 @@
+Ray Tracing in a Weekend in Ruby.
+
+I've made some decisions when porting it.
+Disagree with me if you like. That sounds like your problem.
+
+https://raytracing.github.io/books/RayTracingInOneWeekend.html
diff --git a/lib/hittable.rb b/lib/hittable.rb
new file mode 100644
index 0000000..80254d1
--- /dev/null
+++ b/lib/hittable.rb
@@ -0,0 +1,81 @@
+class HitRecord
+ def initialize(point, t, ray, out_normal)
+ @point = point
+ @t = t
+ set_face_normal(ray, out_normal)
+ end
+
+ attr_accessor :point, :normal, :t
+
+ def set_face_normal(ray, out_normal)
+ @front_face = ray.direction.dot(out_normal) < 0
+ @normal = @front_face ? out_normal : -out_normal
+ end
+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(ox, oy, oz, radius = 1)
+ @centre = Point.new(ox, oy, oz)
+ @radius = radius
+ 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)
+ end
+end
diff --git a/lib/interval.rb b/lib/interval.rb
new file mode 100644
index 0000000..0aa787f
--- /dev/null
+++ b/lib/interval.rb
@@ -0,0 +1,20 @@
+class Interval
+ def initialize(min = Float::INFINITY, max = -Float::INFINITY)
+ @min = min
+ @max = max
+ end
+
+ attr_reader :min, :max
+
+ def size
+ @max - @min
+ end
+
+ def include?(x)
+ min <= x && x <= max
+ end
+
+ def surround?(x)
+ min < x && x < max
+ end
+end
diff --git a/lib/ray.rb b/lib/ray.rb
new file mode 100644
index 0000000..4d282cb
--- /dev/null
+++ b/lib/ray.rb
@@ -0,0 +1,12 @@
+class Ray
+ def initialize(origin = Point.new, direction = Vec3.new)
+ @origin = origin
+ @direction = direction
+ end
+
+ attr_reader :origin, :direction
+
+ def at(time)
+ @origin + (@direction * time)
+ end
+end
diff --git a/lib/vec3.rb b/lib/vec3.rb
new file mode 100644
index 0000000..6287813
--- /dev/null
+++ b/lib/vec3.rb
@@ -0,0 +1,74 @@
+class Vec3
+ def initialize(x = 0, y = 0, z = 0)
+ @x = x.to_f
+ @y = y.to_f
+ @z = z.to_f
+ 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 to_s
+ "{#{@x}, #{@y}, #{@z}}"
+ end
+end
+
+class Point < Vec3; end
+
+class Colour < Vec3
+ def r; @x; end
+ def g; @y; end
+ def b; @z; end
+
+ def to_ppm
+ "%3d %3d %3d" % [(r * 255.999).to_i, (g * 255.999).to_i, (b * 255.999).to_i]
+ end
+end
diff --git a/rtiaw b/rtiaw
new file mode 100755
index 0000000..5fb7e5a
--- /dev/null
+++ b/rtiaw
@@ -0,0 +1,79 @@
+#!/usr/bin/env ruby
+
+start = Time.now.to_f
+
+$LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'
+require 'interval'
+require 'vec3'
+require 'ray'
+require 'hittable'
+
+def ray_colour(ray, world)
+ if rec = world.hit(ray, Interval.new(0, Float::INFINITY))
+ return (Colour.new(1,1,1) + rec.normal) * 0.5
+ end
+
+ unit_dir = ray.direction.unit
+ a = (unit_dir.y + 1) / 2
+ Colour.new(1.0, 1.0, 1.0) * (1-a) + Colour.new(0.5, 0.7, 1.0) * a
+end
+
+# Define image
+width = 400
+aspect = 16.0 / 9
+max = 255
+
+# Calculate height
+height = (width / aspect).to_i
+height = height < 1 ? 1 : height
+
+# World
+world = Hittables.new
+world << Sphere.new(0,0,-1, 0.5)
+world << Sphere.new(0,-100.5,-1, 100)
+
+# Camera
+c_focallength = 1.0
+c_centre = Point.new(0,0,0)
+
+# Viewport
+v_height = 2.0
+v_width = v_height * width.to_f / height
+v_u = Vec3.new(v_width, 0, 0)
+v_v = Vec3.new(0, -v_height, 0)
+
+# Pixel deltas
+pd_u = v_u / width
+pd_v = v_v / height
+
+# Upper left pixel
+v_upperleft = c_centre -
+ Vec3.new(0, 0, c_focallength) -
+ v_u / 2 -
+ v_v / 2
+p_00loc = v_upperleft +
+ (pd_u + pd_v) / 2
+
+puts <<-EOH
+P3
+#{width} #{height}
+#{max}
+EOH
+
+height.times do |row|
+ $stderr.print "Scanlines remaining: %4d" % (height - row)
+ width.times do |col|
+ p_centre = p_00loc + (pd_u * col) + (pd_v * row)
+ ray_dir = p_centre - c_centre
+ ray = Ray.new(c_centre, ray_dir)
+
+ p_colour = ray_colour(ray, world)
+ puts p_colour.to_ppm
+ end
+ puts
+end
+
+finish = Time.now.to_f
+took = finish - start
+
+$stderr.puts "Done. Took %4.2f seconds. " % took