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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/lib/rray/camera.rb +97 -0
- data/lib/rray/color.rb +28 -0
- data/lib/rray/hit.rb +19 -0
- data/lib/rray/interval.rb +25 -0
- data/lib/rray/material/base.rb +11 -0
- data/lib/rray/material/dielectric.rb +43 -0
- data/lib/rray/material/lambertian.rb +23 -0
- data/lib/rray/material/metal.rb +23 -0
- data/lib/rray/object/base.rb +11 -0
- data/lib/rray/object/group.rb +32 -0
- data/lib/rray/object/sphere.rb +44 -0
- data/lib/rray/point3.rb +6 -0
- data/lib/rray/ray.rb +16 -0
- data/lib/rray/scatter.rb +12 -0
- data/lib/rray/scene.rb +72 -0
- data/lib/rray/tracer.rb +63 -0
- data/lib/rray/util.rb +17 -0
- data/lib/rray/vec3.rb +136 -0
- data/lib/rray/version.rb +5 -0
- data/lib/rray.rb +25 -0
- data/sig/rray.rbs +4 -0
- metadata +70 -0
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
data/lib/rray/camera.rb
ADDED
@@ -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,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,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
|
data/lib/rray/point3.rb
ADDED
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
|
data/lib/rray/scatter.rb
ADDED
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
|
data/lib/rray/tracer.rb
ADDED
@@ -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
|
data/lib/rray/version.rb
ADDED
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
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: []
|