rray 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []