rray 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8524e60bb5c72619ed588acd7778faf1c4164b93928094d9c75379e3315a1897
4
+ data.tar.gz: ad9f79b082e057ff74b555672bbc7a11082fda5339353506efa1e004003642a3
5
+ SHA512:
6
+ metadata.gz: f64cf7f310538afd73185ebc926ad928ee727752160012d6d506149a9e1433d1486579ffb8b51ab8fd6385aafc4d5a8b656d41dadcb59cf95babe97789eccdbd
7
+ data.tar.gz: 9379ec81d4072e789115fd3534570733d0cd1d29fa5636e4a67e4f0e6f377e5414fa62fe66dc2cbc4872165cc6c78b85c50c2cb611d7f1a6098998b4f48e8b8b
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Danielle Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Rray
2
+
3
+ Simple raytracer written in ruby.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add rray
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install rray
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require "rray"
19
+
20
+ scene = Rray::Scene.parse(File.read("scene.json"))
21
+ tracer = Rray::Tracer.new(scene, width: 800, height: 600, samples_per_pixel: 50, max_depth: 100)
22
+
23
+ output = Array.new(tracer.height) { Array.new(tracer.width) { [0, 0, 0] } }
24
+ output.each.with_index do |row, i|
25
+ row.each.with_index do |pixel, j|
26
+ r, g, b = tracer.call(j, i)
27
+
28
+ pixel[0] = r
29
+ pixel[1] = g
30
+ pixel[2] = b
31
+ end
32
+ end
33
+
34
+ # store output as an image
35
+ ```
36
+
37
+ ## Development
38
+
39
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
40
+
41
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
42
+
43
+ ## Contributing
44
+
45
+ Bug reports and pull requests are welcome on GitHub at https://github.com/danini-the-panini/rray.
46
+
47
+ ## License
48
+
49
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Camera
5
+ attr_reader \
6
+ :vfov, # Vertical view angle (field of view)
7
+ :lookfrom, # Point camera is looking from
8
+ :lookat, # Point camera is looking at
9
+ :vup, # Camera-relative "up" direction
10
+
11
+ :defocus_angle, # Variation angle of rays through each pixel
12
+ :focus_dist # Distance from camera lookfrom point to plane of perfect focus
13
+
14
+ def initialize(
15
+ vfov: 90.0,
16
+ lookfrom: Point3.new(0.0, 0.0, 0.0),
17
+ lookat: Point3.new(0.0, 0.0, -1.0),
18
+ vup: Vec3.new(0.0, 1.0, 0.0),
19
+ defocus_angle: 0.0,
20
+ focus_dist: 10.0
21
+ )
22
+ @vfov = vfov
23
+ @lookfrom = lookfrom
24
+ @lookat = lookat
25
+ @vup = vup
26
+ @defocus_angle = defocus_angle
27
+ @focus_dist = focus_dist
28
+ end
29
+
30
+ def viewport(image_width, image_height)
31
+ @image_height = image_height
32
+ aspect_ratio = image_width.to_f / image_height.to_f
33
+
34
+ @center = lookfrom
35
+
36
+ # Determine viewport dimensions.
37
+ theta = Util.degrees_to_radians(vfov)
38
+ h = Math.tan(theta / 2.0)
39
+ viewport_height = 2.0 * h * focus_dist
40
+ viewport_width = viewport_height * aspect_ratio
41
+
42
+ # Calculate the u,v,w unit basis vectors for the camera coordinate frame.
43
+ @w = (lookfrom - lookat).normalize
44
+ @u = vup.cross(w).normalize
45
+ @v = w.cross(u)
46
+
47
+ # Calculate the vectors across the horizontal and down the vertical viewport edges.
48
+ viewport_u = u * viewport_width # Vector across viewport horizontal edge
49
+ viewport_v = -v * viewport_height # Vector down viewport vertical edge
50
+
51
+ # Calculate the horizontal and vertical delta vectors from pixel to pixel.
52
+ @pixel_delta_u = viewport_u / image_width.to_f
53
+ @pixel_delta_v = viewport_v / image_height.to_f
54
+
55
+ # Calculate the location of the upper left pixel.
56
+ viewport_upper_left =
57
+ center - (w*focus_dist) - viewport_u.div(2.0) - viewport_v.div(2.0)
58
+ @pixel00_loc = viewport_upper_left.add((pixel_delta_u + pixel_delta_v).mul(0.5))
59
+
60
+ # Calculate the camera defocus disk basis vectors.
61
+ defocus_radius = focus_dist * Math.tan(Util.degrees_to_radians(defocus_angle / 2.0))
62
+ @defocus_disk_u = u * defocus_radius
63
+ @defocus_disk_v = v * defocus_radius
64
+ end
65
+
66
+ # Construct a camera ray originating from the defocus disk and directed at a randomly
67
+ # sampled point around the pixel location x, y.
68
+ def ray(x, y)
69
+ offset = Util.sample_square
70
+ pixel_sample = pixel00_loc +
71
+ (pixel_delta_u * (x + offset.x)) +
72
+ (pixel_delta_v * (y + offset.y))
73
+
74
+ ray_origin = defocus_angle <= 0 ? center : defocus_disk_sample
75
+ ray_direction = pixel_sample - ray_origin
76
+
77
+ Ray.new(ray_origin, ray_direction)
78
+ end
79
+
80
+ private
81
+ attr_reader \
82
+ :image_height, # Rendered image height
83
+ :center, # Camera center
84
+ :pixel00_loc, # Location of pixel 0, 0
85
+ :pixel_delta_u, # Offset to pixel to the right
86
+ :pixel_delta_v, # Offset to pixel below
87
+ :u, :v, :w, # Camera frame basis vectors
88
+ :defocus_disk_u, # Defocus disk horizontal radius
89
+ :defocus_disk_v # Defocus disk vertical radius
90
+
91
+ # Returns a random point in the camera defocus disk.
92
+ def defocus_disk_sample
93
+ v = Vec3.random_in_unit_disk
94
+ center + (defocus_disk_u * v.x) + (defocus_disk_v * v.y)
95
+ end
96
+ end
97
+ end
data/lib/rray/color.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Color < Vec3
5
+ def r = x
6
+ def g = y
7
+ def b = z
8
+
9
+ def r=(v)
10
+ self.x = v
11
+ end
12
+
13
+ def g=(v)
14
+ self.y = v
15
+ end
16
+
17
+ def b=(v)
18
+ self.z = v
19
+ end
20
+
21
+ def multiply(v)
22
+ self.r *= v.r
23
+ self.g *= v.g
24
+ self.b *= v.b
25
+ self
26
+ end
27
+ end
28
+ end
data/lib/rray/hit.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Hit
5
+ attr_accessor :point, :normal, :t, :mat, :front_face
6
+
7
+ def initialize(point, r, outward_normal, t, mat)
8
+ @point = point
9
+ @t = t
10
+ @mat = mat
11
+
12
+ # Sets the hit record normal vector.
13
+ # NOTE: the parameter `outward_normal` is assumed to have unit length.
14
+
15
+ @front_face = r.direction.dot(outward_normal) < 0
16
+ @normal = @front_face ? outward_normal : -outward_normal
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Interval
5
+ attr_accessor :min, :max
6
+
7
+ def initialize(min, max)
8
+ @min = min
9
+ @max = max
10
+ end
11
+
12
+ def size = max - min
13
+ def include?(x) = min <= x && x <= max
14
+ def surround?(x) = min < x && x < max
15
+
16
+ def clamp(x)
17
+ return min if x < min
18
+ return max if x > max
19
+ x
20
+ end
21
+
22
+ EMPTY = new(Float::INFINITY, -Float::INFINITY)
23
+ UNIVERSE = new(-Float::INFINITY, Float::INFINITY)
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Material
5
+ class Base
6
+ def scatter(r_in, rec)
7
+ raise NotImplementedError, "Implement #{self.class}#scatter"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Material
5
+ class Dielectric < Base
6
+ # Refractive index in vacuum or air, or the ratio of the material's refractive index over
7
+ # the refractive index of the enclosing media
8
+ attr_accessor :refraction_index
9
+
10
+ def initialize(refraction_index)
11
+ @refraction_index = refraction_index
12
+ end
13
+
14
+ def scatter(r_in, rec)
15
+ attenuation = Color.new(1.0, 1.0, 1.0)
16
+ ri = rec.front_face ? 1.0/refraction_index : refraction_index
17
+
18
+ unit_direction = r_in.direction.unit
19
+ cos_theta = [(-unit_direction).dot(rec.normal), 1.0].min
20
+ sin_theta = Math.sqrt(1.0 - cos_theta**2)
21
+
22
+ cannot_refract = ri * sin_theta > 1.0
23
+ direction = if cannot_refract || reflectance(cos_theta, ri) > rand
24
+ unit_direction.reflect(rec.normal)
25
+ else
26
+ unit_direction.refract(rec.normal, ri)
27
+ end
28
+
29
+ scattered = Ray.new(rec.point, direction)
30
+
31
+ Scatter.new(scattered, attenuation)
32
+ end
33
+
34
+ private
35
+
36
+ def reflectance(cosine, ri)
37
+ # Use Schlick's approximation for reflectance.
38
+ r0 = ((1.0 - ri) / (1.0 + ri))**2
39
+ r0 + (1-r0)*(1.0-cosine)**5
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Material
5
+ class Lambertian < Base
6
+ attr_accessor :albedo
7
+
8
+ def initialize(albedo)
9
+ @albedo = albedo
10
+ end
11
+
12
+ def scatter(r_in, rec)
13
+ scatter_direction = rec.normal + Vec3.random_unit_vector
14
+ scattered = Ray.new(rec.point, scatter_direction)
15
+
16
+ # Catch degenerate scatter direction
17
+ scatter_direction = rec.normal if scatter_direction.zero?
18
+
19
+ Scatter.new(scattered, albedo)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Material
5
+ class Metal < Base
6
+ attr_accessor :albedo, :fuzz
7
+
8
+ def initialize(albedo, fuzz)
9
+ @albedo = albedo
10
+ @fuzz = fuzz
11
+ end
12
+
13
+ def scatter(r_in, rec)
14
+ reflected = r_in.direction.reflect(rec.normal)
15
+ reflected = reflected.normalize.add(Vec3.random_unit_vector.mul(fuzz))
16
+ scattered = Ray.new(rec.point, reflected)
17
+ return nil if scattered.direction.dot(rec.normal) <= 0
18
+
19
+ Scatter.new(scattered, albedo)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Object
5
+ class Base
6
+ def hit(r, ray_t)
7
+ raise NotImplementedError, "Implement #{self.class.name}#hit"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Object
5
+ class Group < Base
6
+ attr_reader :objects
7
+
8
+ def initialize(objects = [])
9
+ @objects = objects
10
+ end
11
+
12
+ def <<(object)
13
+ objects << object
14
+ end
15
+
16
+ def hit(r, ray_t)
17
+ rec = nil
18
+ closest_so_far = ray_t.end
19
+
20
+ objects.each do |object|
21
+ temp_rec = object.hit(r, ray_t.begin..closest_so_far)
22
+ next unless temp_rec
23
+
24
+ rec = temp_rec
25
+ closest_so_far = rec.t
26
+ end
27
+
28
+ rec
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Object
5
+ class Sphere < Base
6
+ attr_accessor :center, :radius, :mat
7
+
8
+ def initialize(center, radius, mat)
9
+ @center = center
10
+ @radius = radius
11
+ @mat = mat
12
+ end
13
+
14
+ def hit(r, ray_t)
15
+ oc = center - r.origin
16
+ a = r.direction.length_squared
17
+ h = r.direction.dot(oc)
18
+ c = oc.length_squared - radius**2
19
+
20
+ discriminant = h**2 - a*c
21
+ return nil if discriminant < 0
22
+
23
+ sqrtd = Math.sqrt(discriminant)
24
+
25
+ # Find the nearest root that lies in the acceptable range.
26
+ root = (h - sqrtd) / a
27
+ unless ray_t.cover?(root)
28
+ root = (h + sqrtd) / a
29
+ return nil unless ray_t.cover?(root)
30
+ end
31
+
32
+ point = r.at(root)
33
+
34
+ Hit.new(
35
+ point,
36
+ r,
37
+ (point - center).div(radius),
38
+ root,
39
+ mat
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Point3 < Vec3
5
+ end
6
+ end
data/lib/rray/ray.rb ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Ray
5
+ attr_accessor :origin, :direction
6
+
7
+ def initialize(origin, direction)
8
+ @origin = origin
9
+ @direction = direction
10
+ end
11
+
12
+ def at(t) = (direction*t).add(origin)
13
+
14
+ def dup = self.class.new(origin, direction)
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Scatter
5
+ attr_accessor :ray, :attenuation
6
+
7
+ def initialize(ray, attenuation)
8
+ @ray = ray
9
+ @attenuation = attenuation
10
+ end
11
+ end
12
+ end
data/lib/rray/scene.rb ADDED
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Rray
6
+ class Scene
7
+ attr_reader :world, :camera
8
+
9
+ def initialize(world, camera)
10
+ @world = world
11
+ @camera = camera
12
+ end
13
+
14
+ def self.parse(json)
15
+ return parse(JSON.parse(json)) if json.is_a? String
16
+
17
+ Parser.new(json).parse
18
+ end
19
+
20
+ class Parser
21
+ class Error < Rray::Error; end
22
+
23
+ attr_reader :json
24
+
25
+ def initialize(json)
26
+ @json = json
27
+ end
28
+
29
+ def parse
30
+ @materials = json["materials"].transform_values { parse_material(_1) }
31
+ world = parse_object(json["world"])
32
+ camera = parse_camera(json["camera"])
33
+
34
+ Scene.new(world, camera)
35
+ end
36
+
37
+ def parse_material(mat)
38
+ case mat["type"]
39
+ when "Lambertian" then Material::Lambertian.new(Color.new(*mat["albedo"]))
40
+ when "Metal" then Material::Metal.new(Color.new(*mat["albedo"]), mat["fuzz"])
41
+ when "Dielectric" then Material::Dielectric.new(mat["refractive_index"])
42
+ else raise ParserError, "unknown material type #{mat["type"]}"
43
+ end
44
+ end
45
+
46
+ def parse_object(obj)
47
+ case obj["type"]
48
+ when "Group" then Object::Group.new(obj["objects"].map { parse_object(_1) })
49
+ when "Sphere" then Object::Sphere.new(Point3.new(*obj["center"]), obj["radius"], material(obj["material"]))
50
+ else raise ParserError, "unknown object type #{obj["type"]}"
51
+ end
52
+ end
53
+
54
+ def material(name)
55
+ raise ParserError, "unknown material #{name}" unless @materials.key?(name)
56
+
57
+ @materials[name]
58
+ end
59
+
60
+ def parse_camera(cam)
61
+ args = {}
62
+ args[:vfov] = cam["vfov"] if cam.key?("vfov")
63
+ args[:lookfrom] = Point3.new(*cam["lookfrom"]) if cam.key?("lookfrom")
64
+ args[:lookat] = Point3.new(*cam["lookat"]) if cam.key?("lookat")
65
+ args[:vup] = Vec3.new(*cam["vup"]) if cam.key?("vup")
66
+ args[:defocus_angle] = cam["defocus_angle"] if cam.key?("defocus_angle")
67
+ args[:focus_dist] = cam["focus_dist"] if cam.key?("focus_dist")
68
+ Camera.new(**args)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Tracer
5
+ attr_reader :scene, :width, :height, :samples_per_pixel, :max_depth
6
+
7
+ def initialize(scene, width: 256, height: 256, samples_per_pixel: 10, max_depth: 10)
8
+ @scene = scene
9
+ @width = width
10
+ @height = height
11
+ @samples_per_pixel = samples_per_pixel
12
+ @max_depth = max_depth
13
+ @pixel_sample_scale = 1.0 / samples_per_pixel
14
+
15
+ scene.camera.viewport(width, height)
16
+ end
17
+
18
+ def call(x, y)
19
+ pixel_color = Color.new(0.0, 0.0, 0.0)
20
+ samples_per_pixel.times do
21
+ pixel_color.add(ray_color(scene.camera.ray(x, y)))
22
+ end
23
+ color(pixel_color.mul(@pixel_sample_scale))
24
+ end
25
+
26
+ def ray_color(r, depth = max_depth)
27
+ # If we've exceeded the ray bounce limit, no more light is gathered.
28
+ return Color.new(0.0, 0.0, 0.0) unless depth > 0
29
+
30
+ rec = scene.world.hit(r, 0.001..Float::INFINITY)
31
+ if rec
32
+ scattered = rec.mat.scatter(r, rec)
33
+ return Color.new(0.0, 0.0, 0.0) unless scattered
34
+
35
+ return ray_color(scattered.ray, depth-1).multiply(scattered.attenuation)
36
+ end
37
+
38
+ unit_direction = r.direction.unit
39
+ a = 0.5*(unit_direction.y + 1.0)
40
+ Color.new(1.0, 1.0, 1.0).mul(1.0-a).add(Color.new(0.5, 0.7, 1.0).mul(a))
41
+ end
42
+
43
+ def color(pixel_color)
44
+ r = pixel_color.r
45
+ g = pixel_color.g
46
+ b = pixel_color.b
47
+
48
+ # Apply a linear to gamma transform for gamma 2
49
+ r = Util.linear_to_gamma(r)
50
+ g = Util.linear_to_gamma(g)
51
+ b = Util.linear_to_gamma(b)
52
+
53
+ # Translate the [0,1] component values to the byte range [0,255].
54
+ intensity = Interval.new(0.000, 0.999)
55
+ rbyte = (256 * intensity.clamp(r)).to_i
56
+ gbyte = (256 * intensity.clamp(g)).to_i
57
+ bbyte = (256 * intensity.clamp(b)).to_i
58
+
59
+ # Return the pixel color components.
60
+ [rbyte, gbyte, bbyte]
61
+ end
62
+ end
63
+ end
data/lib/rray/util.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ module Util
5
+ def self.degrees_to_radians(degrees) = degrees * Math::PI / 180.0
6
+
7
+ # Returns a random real in [min,max).
8
+ def self.random(min, max) = min + (max-min)*rand
9
+ def self.sample_square = Vec3.new(rand - 0.5, rand - 0.5, 0.0)
10
+
11
+ def self.linear_to_gamma(linear_component)
12
+ return 0 unless linear_component > 0
13
+
14
+ Math.sqrt(linear_component)
15
+ end
16
+ end
17
+ end
data/lib/rray/vec3.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ class Vec3
5
+ def initialize(x = 0.0, y = 0.0, z = 0.0)
6
+ @e = [x, y, z]
7
+ end
8
+
9
+ def x = @e[0]
10
+ def y = @e[1]
11
+ def z = @e[2]
12
+
13
+ def x=(v)
14
+ @e[0] = v
15
+ end
16
+
17
+ def y=(v)
18
+ @e[1] = v
19
+ end
20
+
21
+ def z=(v)
22
+ @e[2] = v
23
+ end
24
+
25
+ def -@ = self.class.new(-x, -y, -z)
26
+ def [](i) = @e[i]
27
+
28
+ def []=(i, v)
29
+ @e[i] = v
30
+ end
31
+
32
+ def +(v) = Vec3.new(x+v.x, y+v.y, z+v.z)
33
+ def -(v) = Vec3.new(x-v.x, y-v.y, z-v.z)
34
+ def *(t) = Vec3.new(x*t, y*t, z*t)
35
+ def /(t) = self * (1.0/t)
36
+
37
+ def add(v)
38
+ self.x += v.x
39
+ self.y += v.y
40
+ self.z += v.z
41
+ self
42
+ end
43
+
44
+ def sub(v)
45
+ self.x -= v.x
46
+ self.y -= v.y
47
+ self.z -= v.z
48
+ self
49
+ end
50
+
51
+ def mul(t)
52
+ self.x *= t
53
+ self.y *= t
54
+ self.z *= t
55
+ self
56
+ end
57
+
58
+ def div(t)
59
+ mul(1.0/t)
60
+ self
61
+ end
62
+
63
+ def neg
64
+ self.x = -x
65
+ self.y = -y
66
+ self.z = -z
67
+ self
68
+ end
69
+
70
+ def length = Math.sqrt(length_squared)
71
+ def length_squared = dot(self)
72
+
73
+ def dot(v) = x*v.x + y*v.y + z*v.z
74
+
75
+ def cross(v)
76
+ Vec3.new(
77
+ y*v.z - z*v.y,
78
+ z*v.x - x*v.z,
79
+ x*v.y - y*v.x
80
+ )
81
+ end
82
+
83
+ def unit = self / length
84
+
85
+ def normalize
86
+ div(length)
87
+ self
88
+ end
89
+
90
+ def dup = self.class.new(*@e)
91
+
92
+ EPSILON = 1e-8
93
+ # Return true if the vector is close to zero in all dimensions.
94
+ def zero? = x.abs < EPSILON && y.abs < EPSILON && z.abs < EPSILON
95
+
96
+ def reflect(n)
97
+ self - n*(dot(n)*2.0)
98
+ end
99
+
100
+ def refract(n, etai_over_etat)
101
+ cos_theta = [(-self).dot(n), 1.0].min
102
+ r_out_perp = (self + n*cos_theta).mul(etai_over_etat)
103
+ r_out_parallel = n * -Math.sqrt((1.0 - r_out_perp.length_squared).abs)
104
+ r_out_perp.add(r_out_parallel)
105
+ end
106
+
107
+ def self.random(min = 0.0, max = 1.0)
108
+ Vec3.new(Util.random(min, max), Util.random(min, max), Util.random(min, max))
109
+ end
110
+
111
+ def self.random_in_unit_sphere
112
+ loop do
113
+ v = Vec3.random(-1.0, 1.0)
114
+ return v if v.length_squared < 1
115
+ end
116
+ end
117
+
118
+ def self.random_unit_vector = random_in_unit_sphere.normalize
119
+
120
+ def self.random_on_hemisphere(normal)
121
+ on_unit_sphere = random_unit_vector
122
+ if on_unit_sphere.dot(normal) > 0.0 # In the same hemisphere as the normal
123
+ on_unit_sphere
124
+ else
125
+ -on_unit_sphere
126
+ end
127
+ end
128
+
129
+ def self.random_in_unit_disk
130
+ loop do
131
+ v = Vec3.new(Util.random(-1.0, 1.0), Util.random(-1.0, 1.0), 0.0)
132
+ return v if v.length_squared < 1
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rray
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rray.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rray/version"
4
+ require_relative "rray/util"
5
+ require_relative "rray/vec3"
6
+ require_relative "rray/point3"
7
+ require_relative "rray/color"
8
+ require_relative "rray/ray"
9
+ require_relative "rray/interval"
10
+ require_relative "rray/hit"
11
+ require_relative "rray/scatter"
12
+ require_relative "rray/object/base"
13
+ require_relative "rray/object/sphere"
14
+ require_relative "rray/object/group"
15
+ require_relative "rray/material/base"
16
+ require_relative "rray/material/lambertian"
17
+ require_relative "rray/material/metal"
18
+ require_relative "rray/material/dielectric"
19
+ require_relative "rray/camera"
20
+ require_relative "rray/scene"
21
+ require_relative "rray/tracer"
22
+
23
+ module Rray
24
+ class Error < StandardError; end
25
+ end
data/sig/rray.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rray
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rray
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Danielle Smith
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-04-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Simple raytracer written in ruby. Can be used a CLI tool or library.
14
+ email:
15
+ - code@danini.dev
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - Rakefile
23
+ - lib/rray.rb
24
+ - lib/rray/camera.rb
25
+ - lib/rray/color.rb
26
+ - lib/rray/hit.rb
27
+ - lib/rray/interval.rb
28
+ - lib/rray/material/base.rb
29
+ - lib/rray/material/dielectric.rb
30
+ - lib/rray/material/lambertian.rb
31
+ - lib/rray/material/metal.rb
32
+ - lib/rray/object/base.rb
33
+ - lib/rray/object/group.rb
34
+ - lib/rray/object/sphere.rb
35
+ - lib/rray/point3.rb
36
+ - lib/rray/ray.rb
37
+ - lib/rray/scatter.rb
38
+ - lib/rray/scene.rb
39
+ - lib/rray/tracer.rb
40
+ - lib/rray/util.rb
41
+ - lib/rray/vec3.rb
42
+ - lib/rray/version.rb
43
+ - sig/rray.rbs
44
+ homepage: https://github.com/danini-the-panini/rray
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/danini-the-panini/rray
49
+ source_code_uri: https://github.com/danini-the-panini/rray
50
+ changelog_uri: https://github.com/danini-the-panini/rray/releases
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.0.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.5.3
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Raytracer in Ruby
70
+ test_files: []