rray 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|