raysetta 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 +72 -0
- data/Rakefile +8 -0
- data/Steepfile +15 -0
- data/exe/raysetta +3 -0
- data/lib/raysetta/aabb.rb +116 -0
- data/lib/raysetta/background/base.rb +19 -0
- data/lib/raysetta/background/cube_map.rb +44 -0
- data/lib/raysetta/background/gradient.rb +28 -0
- data/lib/raysetta/background/solid.rb +24 -0
- data/lib/raysetta/background/sphere_map.rb +30 -0
- data/lib/raysetta/camera.rb +109 -0
- data/lib/raysetta/entity_base.rb +28 -0
- data/lib/raysetta/exe.rb +75 -0
- data/lib/raysetta/hit.rb +38 -0
- data/lib/raysetta/image.rb +46 -0
- data/lib/raysetta/interval.rb +63 -0
- data/lib/raysetta/material/base.rb +19 -0
- data/lib/raysetta/material/dielectric.rb +60 -0
- data/lib/raysetta/material/diffuse_light.rb +42 -0
- data/lib/raysetta/material/lambertian.rb +48 -0
- data/lib/raysetta/material/metal.rb +49 -0
- data/lib/raysetta/object/base.rb +23 -0
- data/lib/raysetta/object/box.rb +50 -0
- data/lib/raysetta/object/bvh.rb +81 -0
- data/lib/raysetta/object/group.rb +59 -0
- data/lib/raysetta/object/moving_sphere.rb +41 -0
- data/lib/raysetta/object/quad.rb +88 -0
- data/lib/raysetta/object/sphere.rb +68 -0
- data/lib/raysetta/object/tri.rb +46 -0
- data/lib/raysetta/output/base.rb +23 -0
- data/lib/raysetta/output/png.rb +26 -0
- data/lib/raysetta/output/ppm.rb +19 -0
- data/lib/raysetta/perlin.rb +91 -0
- data/lib/raysetta/ray.rb +28 -0
- data/lib/raysetta/runner/base.rb +28 -0
- data/lib/raysetta/runner/concurrent.rb +17 -0
- data/lib/raysetta/runner/processes.rb +26 -0
- data/lib/raysetta/runner/ractors.rb +32 -0
- data/lib/raysetta/runner/sync.rb +28 -0
- data/lib/raysetta/runner/threads.rb +26 -0
- data/lib/raysetta/runner.rb +45 -0
- data/lib/raysetta/scatter.rb +12 -0
- data/lib/raysetta/scene.rb +205 -0
- data/lib/raysetta/texture/base.rb +19 -0
- data/lib/raysetta/texture/checker.rb +55 -0
- data/lib/raysetta/texture/image.rb +57 -0
- data/lib/raysetta/texture/noise.rb +46 -0
- data/lib/raysetta/texture/solid_color.rb +36 -0
- data/lib/raysetta/tracer.rb +43 -0
- data/lib/raysetta/util.rb +38 -0
- data/lib/raysetta/vec2.rb +125 -0
- data/lib/raysetta/vec3.rb +214 -0
- data/lib/raysetta/version.rb +5 -0
- data/lib/raysetta.rb +44 -0
- data/mise.toml +2 -0
- data/scenes/box.json +26 -0
- data/scenes/cornell.json +108 -0
- data/scenes/cubemap.json +109 -0
- data/scenes/dumb.json +58 -0
- data/scenes/earth.json +79 -0
- data/scenes/example.json +11712 -0
- data/scenes/perlin.json +2126 -0
- data/scenes/quads.json +38 -0
- data/scenes/simple_light.json +2126 -0
- data/scenes/testcube.json +113 -0
- data/sig/raysetta/aabb.rbs +39 -0
- data/sig/raysetta/background/base.rbs +11 -0
- data/sig/raysetta/background/cube_map.rbs +9 -0
- data/sig/raysetta/background/gradient.rbs +10 -0
- data/sig/raysetta/background/solid.rbs +9 -0
- data/sig/raysetta/background/sphere_map.rbs +9 -0
- data/sig/raysetta/camera.rbs +41 -0
- data/sig/raysetta/entity_base.rbs +12 -0
- data/sig/raysetta/hit.rbs +21 -0
- data/sig/raysetta/image.rbs +13 -0
- data/sig/raysetta/interval.rbs +36 -0
- data/sig/raysetta/material/base.rbs +11 -0
- data/sig/raysetta/material/dielectric.rbs +15 -0
- data/sig/raysetta/material/diffuse_light.rbs +12 -0
- data/sig/raysetta/material/lambertian.rbs +10 -0
- data/sig/raysetta/material/metal.rbs +11 -0
- data/sig/raysetta/object/base.rbs +15 -0
- data/sig/raysetta/object/box.rbs +13 -0
- data/sig/raysetta/object/bvh.rbs +23 -0
- data/sig/raysetta/object/group.rbs +16 -0
- data/sig/raysetta/object/moving_sphere.rbs +15 -0
- data/sig/raysetta/object/quad.rbs +26 -0
- data/sig/raysetta/object/sphere.rbs +14 -0
- data/sig/raysetta/object/tri.rbs +15 -0
- data/sig/raysetta/output/base.rbs +15 -0
- data/sig/raysetta/output/png.rbs +8 -0
- data/sig/raysetta/output/ppm.rbs +7 -0
- data/sig/raysetta/perlin.rbs +19 -0
- data/sig/raysetta/ray.rbs +18 -0
- data/sig/raysetta/runner/base.rbs +15 -0
- data/sig/raysetta/runner/concurrent.rbs +10 -0
- data/sig/raysetta/runner/processes.rbs +9 -0
- data/sig/raysetta/runner/ractors.rbs +11 -0
- data/sig/raysetta/runner/sync.rbs +11 -0
- data/sig/raysetta/runner/threads.rbs +9 -0
- data/sig/raysetta/runner.rbs +5 -0
- data/sig/raysetta/scatter.rbs +8 -0
- data/sig/raysetta/scene.rbs +69 -0
- data/sig/raysetta/texture/base.rbs +11 -0
- data/sig/raysetta/texture/checker.rbs +15 -0
- data/sig/raysetta/texture/image.rbs +11 -0
- data/sig/raysetta/texture/noise.rbs +13 -0
- data/sig/raysetta/texture/solid_color.rbs +10 -0
- data/sig/raysetta/tracer.rbs +16 -0
- data/sig/raysetta/util.rbs +15 -0
- data/sig/raysetta/vec2.rbs +48 -0
- data/sig/raysetta/vec3.rbs +67 -0
- data/sig/raysetta.rbs +6 -0
- data/sig/stdlib/chunky_png.rbs +24 -0
- data/sig/stdlib/etc.rbs +3 -0
- data/sig/stdlib/json.rbs +3 -0
- data/sig/stdlib/optparse.rbs +4 -0
- data/sig/stdlib/parallel.rbs +5 -0
- data/sig/stdlib/progress.rbs +5 -0
- metadata +206 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 81bfef72146fa74b652618060ed392bcba82b47d4abe0b5f614b562ce5990eea
|
|
4
|
+
data.tar.gz: a79b00b4f6eee82d6b2820abbeab125c88af5cef8f3b605ec8af77b9afacd31e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 62136d552f0fc48578c1f0e6079ed9b9ee5f1b57ee3dda05834f63fee7d91d6adbf2f0981795bb53d42bc371fa3b9b8445a39b5de14c168539f93fb2dff0ef27
|
|
7
|
+
data.tar.gz: a62389f792764154a40c43529229f319da34f4a0545d59d12839efcfaf17b130660f1f93df5823e94148ae67f91611ef335a40fbaced0f61e610b6ea2816efdc
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 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,72 @@
|
|
|
1
|
+
# Raysetta Ruby
|
|
2
|
+
|
|
3
|
+
A raytracer written in Ruby. Part of the Raysetta project. Based on [Ray Tracing in One Weekend](https://raytracing.github.io/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
8
|
+
|
|
9
|
+
$ bundle add raysetta
|
|
10
|
+
|
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
12
|
+
|
|
13
|
+
$ gem install raysetta
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### CLI
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$ raysetta scene.json --width 800 --height 600 --samples 50 --depth 100 --format ppm -o output.ppm
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Gem
|
|
24
|
+
|
|
25
|
+
#### Raytracer
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "raysetta"
|
|
29
|
+
|
|
30
|
+
scene = Raysetta::Scene.parse(File.read("scene.json"))
|
|
31
|
+
tracer = Raysetta::Tracer.new(scene, width: 800, height: 600, samples_per_pixel: 50, max_depth: 100)
|
|
32
|
+
|
|
33
|
+
output = Array.new(tracer.height) { Array.new(tracer.width) { [0, 0, 0] } }
|
|
34
|
+
output.each.with_index do |row, i|
|
|
35
|
+
row.each.with_index do |pixel, j|
|
|
36
|
+
r, g, b = tracer.call(j, i)
|
|
37
|
+
|
|
38
|
+
pixel[0] = r
|
|
39
|
+
pixel[1] = g
|
|
40
|
+
pixel[2] = b
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# store output as an image
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Runner
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "raysetta/runner"
|
|
51
|
+
|
|
52
|
+
scene = Raysetta::Scene.parse(File.read("scene.json"))
|
|
53
|
+
tracer = Raysetta::Tracer.new(scene, width: 800, height: 600, samples_per_pixel: 50, max_depth: 100)
|
|
54
|
+
runner = Raysetta::Runner::Sync.new(tracer)
|
|
55
|
+
runner.call
|
|
56
|
+
out = Raysetta::Output::PPM.new(runner.output, width: tracer.width, height: tracer.height)
|
|
57
|
+
out.save("output.ppm")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
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).
|
|
65
|
+
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/danini-the-panini/raysetta-ruby.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/Steepfile
ADDED
data/exe/raysetta
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './interval'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
class AABB
|
|
7
|
+
attr_accessor :x, :y, :z
|
|
8
|
+
|
|
9
|
+
# The default AABB is empty, since intervals are empty by default.
|
|
10
|
+
def initialize(x = Interval.new, y = Interval.new, z = Interval.new)
|
|
11
|
+
@x = x
|
|
12
|
+
@y = y
|
|
13
|
+
@z = z
|
|
14
|
+
pad_to_minimums
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Treat the two points a and b as extrema for the bounding box, so we don't require a
|
|
18
|
+
# particular minimum/maximum coordinate order.
|
|
19
|
+
def self.from_points(*points)
|
|
20
|
+
x = points.map(&:x).minmax #: [Float, Float]
|
|
21
|
+
y = points.map(&:y).minmax #: [Float, Float]
|
|
22
|
+
z = points.map(&:z).minmax #: [Float, Float]
|
|
23
|
+
new(Interval.new(x[0], x[1]), Interval.new(y[0], y[1]), Interval.new(z[0], z[1]))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_aabbs(box0, box1=nil, *rest)
|
|
27
|
+
return box0 if box1.nil?
|
|
28
|
+
|
|
29
|
+
self.from_aabbs(box0 + box1, *rest)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](n)
|
|
33
|
+
case n
|
|
34
|
+
when 0 then x
|
|
35
|
+
when 1 then y
|
|
36
|
+
else z
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def []=(n, v)
|
|
41
|
+
case n
|
|
42
|
+
when 0 then self.x = v
|
|
43
|
+
when 1 then self.y = v
|
|
44
|
+
when 2 then self.z = v
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def +(other)
|
|
49
|
+
AABB.new(
|
|
50
|
+
Interval.from_intervals(x, other.x),
|
|
51
|
+
Interval.from_intervals(y, other.y),
|
|
52
|
+
Interval.from_intervals(z, other.z)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def hit(r, ray_t)
|
|
57
|
+
t = Interval.from_range(ray_t)
|
|
58
|
+
|
|
59
|
+
(0...3).each do |axis|
|
|
60
|
+
ax = self[axis]
|
|
61
|
+
adinv = 1.0 / r.direction[axis]
|
|
62
|
+
|
|
63
|
+
t0 = (ax.min - r.origin[axis]) * adinv
|
|
64
|
+
t1 = (ax.max - r.origin[axis]) * adinv
|
|
65
|
+
|
|
66
|
+
if t0 < t1
|
|
67
|
+
t.min = t0 if t0 > t.min
|
|
68
|
+
t.max = t1 if t1 < t.max
|
|
69
|
+
else
|
|
70
|
+
t.min = t1 if t1 > t.min
|
|
71
|
+
t.max = t0 if t0 < t.max
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
return false if t.max <= t.min
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the index of the longest axis of the bounding box.
|
|
81
|
+
def longest_axis
|
|
82
|
+
if x.size > y.size
|
|
83
|
+
x.size > z.size ? 0 : 2
|
|
84
|
+
else
|
|
85
|
+
y.size > z.size ? 1 : 2
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def dup
|
|
90
|
+
AABB.new(x.dup, y.dup, z.dup)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ==(box)
|
|
94
|
+
x == box.x && y == box.y && z == box.z
|
|
95
|
+
end
|
|
96
|
+
alias :eql? :==
|
|
97
|
+
|
|
98
|
+
def hash
|
|
99
|
+
[x, y, z].hash
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Adjust the AABB so that no side is narrower than some delta, padding if necessary.
|
|
105
|
+
def pad_to_minimums
|
|
106
|
+
delta = 0.0001
|
|
107
|
+
x.expand!(delta) if x.size < delta
|
|
108
|
+
y.expand!(delta) if y.size < delta
|
|
109
|
+
z.expand!(delta) if z.size < delta
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
EMPTY = new(Interval::EMPTY, Interval::EMPTY, Interval::EMPTY)
|
|
113
|
+
UNIVERSE = new(Interval::UNIVERSE, Interval::UNIVERSE, Interval::UNIVERSE)
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Background
|
|
5
|
+
class Base < EntityBase
|
|
6
|
+
def sample(r)
|
|
7
|
+
raise NotImplementedError, "Implement #{self.class.name}#sample"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def textures
|
|
11
|
+
[]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def images
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Background
|
|
5
|
+
class CubeMap < Base
|
|
6
|
+
attr_accessor :textures
|
|
7
|
+
|
|
8
|
+
def initialize(*textures)
|
|
9
|
+
@textures = textures
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sample(r)
|
|
13
|
+
v = r.direction
|
|
14
|
+
v_abs = v.abs
|
|
15
|
+
if(v_abs.z >= v_abs.x && v_abs.z >= v_abs.y)
|
|
16
|
+
# front/back
|
|
17
|
+
face_index = v.z < 0.0 ? 5 : 4
|
|
18
|
+
ma = 0.5 / v_abs.z
|
|
19
|
+
uv = Vec2.new(v.z < 0.0 ? -v.x : v.x, v.y)
|
|
20
|
+
elsif(v_abs.y >= v_abs.x)
|
|
21
|
+
# top/bottom
|
|
22
|
+
face_index = v.y < 0.0 ? 3 : 2
|
|
23
|
+
ma = 0.5 / v_abs.y
|
|
24
|
+
uv = Vec2.new(v.x, v.y < 0.0 ? v.z : -v.z)
|
|
25
|
+
else
|
|
26
|
+
# left/right
|
|
27
|
+
face_index = v.x < 0.0 ? 1 : 0
|
|
28
|
+
ma = 0.5 / v_abs.x
|
|
29
|
+
uv = Vec2.new(v.x < 0.0 ? v.z : -v.z, v.y)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
uv = uv * ma + Vec2.new(0.5, 0.5)
|
|
33
|
+
textures[face_index].sample(uv, v.unit)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def export
|
|
37
|
+
{
|
|
38
|
+
**super,
|
|
39
|
+
textures: textures.map(&:id)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Background
|
|
5
|
+
class Gradient < Base
|
|
6
|
+
attr_accessor :top, :bottom
|
|
7
|
+
|
|
8
|
+
def initialize(top, bottom)
|
|
9
|
+
@top = top
|
|
10
|
+
@bottom = bottom
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def sample(r)
|
|
14
|
+
unit_direction = r.direction.unit
|
|
15
|
+
a = 0.5 * (unit_direction.y + 1.0)
|
|
16
|
+
bottom * (1.0 - a) + top * a
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def export
|
|
20
|
+
{
|
|
21
|
+
**super,
|
|
22
|
+
top: top.to_a,
|
|
23
|
+
bottom: bottom.to_a
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Background
|
|
5
|
+
class Solid < Base
|
|
6
|
+
attr_accessor :albedo
|
|
7
|
+
|
|
8
|
+
def initialize(albedo)
|
|
9
|
+
@albedo = albedo
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sample(r)
|
|
13
|
+
albedo
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def export
|
|
17
|
+
{
|
|
18
|
+
**super,
|
|
19
|
+
albedo: albedo.to_a
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Background
|
|
5
|
+
class SphereMap < Base
|
|
6
|
+
attr_accessor :texture
|
|
7
|
+
|
|
8
|
+
def initialize(texture)
|
|
9
|
+
@texture = texture
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sample(r)
|
|
13
|
+
unit_direction = r.direction.unit
|
|
14
|
+
uv = Util.sphere_uv(unit_direction)
|
|
15
|
+
texture.sample(uv, unit_direction)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def textures
|
|
19
|
+
[texture]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def export
|
|
23
|
+
{
|
|
24
|
+
**super,
|
|
25
|
+
texture: texture.id
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
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: Vec3.new(0.0, 0.0, 0.0),
|
|
17
|
+
lookat: Vec3.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 = Vec2.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
|
+
ray_time = rand
|
|
77
|
+
|
|
78
|
+
Ray.new(ray_origin, ray_direction, ray_time)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def export
|
|
82
|
+
{
|
|
83
|
+
vfov: vfov,
|
|
84
|
+
lookfrom: lookfrom.to_a,
|
|
85
|
+
lookat: lookat.to_a,
|
|
86
|
+
vup: vup.to_a,
|
|
87
|
+
defocus_angle: defocus_angle,
|
|
88
|
+
focus_dist: focus_dist
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
attr_reader \
|
|
94
|
+
:image_height, # Rendered image height
|
|
95
|
+
:center, # Camera center
|
|
96
|
+
:pixel00_loc, # Location of pixel 0, 0
|
|
97
|
+
:pixel_delta_u, # Offset to pixel to the right
|
|
98
|
+
:pixel_delta_v, # Offset to pixel below
|
|
99
|
+
:u, :v, :w, # Camera frame basis vectors
|
|
100
|
+
:defocus_disk_u, # Defocus disk horizontal radius
|
|
101
|
+
:defocus_disk_v # Defocus disk vertical radius
|
|
102
|
+
|
|
103
|
+
# Returns a random point in the camera defocus disk.
|
|
104
|
+
def defocus_disk_sample
|
|
105
|
+
v = Vec3.random_in_unit_disk
|
|
106
|
+
center + (defocus_disk_u * v.x) + (defocus_disk_v * v.y)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
class EntityBase
|
|
5
|
+
def id
|
|
6
|
+
"#{type.downcase}-#{object_id.to_s(16)}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def type
|
|
10
|
+
self.class.name.split('::').last
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ==(other)
|
|
14
|
+
other.class == self.class && id == other.id
|
|
15
|
+
end
|
|
16
|
+
alias :eql? :==
|
|
17
|
+
|
|
18
|
+
def hash
|
|
19
|
+
id.hash
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def export
|
|
23
|
+
{
|
|
24
|
+
type: type
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/raysetta/exe.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'etc'
|
|
3
|
+
|
|
4
|
+
format_opt = :ppm
|
|
5
|
+
runner_opt = :sync
|
|
6
|
+
concurrency_opt = nil
|
|
7
|
+
output_path = nil
|
|
8
|
+
options = {} #: Hash[Symbol, untyped]
|
|
9
|
+
opts_parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: raysetta FILE [options]"
|
|
11
|
+
|
|
12
|
+
opts.on("-w", "--width WIDTH", Integer, "Image width (default 256)") do |w|
|
|
13
|
+
options[:width] = w
|
|
14
|
+
end
|
|
15
|
+
opts.on("-h", "--height HEIGHT", Integer, "Image height (default 256)") do |h|
|
|
16
|
+
options[:height] = h
|
|
17
|
+
end
|
|
18
|
+
opts.on("-s", "--samples SAMPLES", Integer, "Samples per pixel (default 10)") do |s|
|
|
19
|
+
options[:samples_per_pixel] = s
|
|
20
|
+
end
|
|
21
|
+
opts.on("-d", "--depth DEPTH", Integer, "Max depth (default 10)") do |d|
|
|
22
|
+
options[:max_depth] = d
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on("-f", "--format FORMAT", %i[ppm png], "Output format (ppm, png; default ppm)") do |f|
|
|
26
|
+
format_opt = f
|
|
27
|
+
end
|
|
28
|
+
opts.on("-r", "--runner RUNNER", %i[sync threads ractors processes], "Runner (sync, threads, ractors, processes; default sync)") do |r|
|
|
29
|
+
runner_opt = r
|
|
30
|
+
end
|
|
31
|
+
opts.on("-c", "--concurrency CONCURRENCY", Integer, "Concurrency (threads and ractor only; default 4)") do |c|
|
|
32
|
+
concurrency_opt = c
|
|
33
|
+
end
|
|
34
|
+
opts.on("-o", "--output FILE", "Output file (default STDOUT)") do |o|
|
|
35
|
+
output_path = o
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on_tail("--help", "Show this message") do
|
|
39
|
+
puts opts
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on_tail("-v", "--version", "Show version") do
|
|
44
|
+
puts Raysetta::VERSION
|
|
45
|
+
exit
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
opts_parser.parse!
|
|
49
|
+
|
|
50
|
+
require "raysetta"
|
|
51
|
+
require "raysetta/runner"
|
|
52
|
+
|
|
53
|
+
if ARGV.size < 1
|
|
54
|
+
warn "*** Missing FILE_OR_URL argument ***"
|
|
55
|
+
puts opts_parser
|
|
56
|
+
exit
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
file_or_url = ARGV.first
|
|
60
|
+
|
|
61
|
+
warn "WARN: Ignoring concurrency argument" if concurrency_opt && runner_opt == :sync
|
|
62
|
+
concurrency = concurrency_opt || Etc.nprocessors
|
|
63
|
+
|
|
64
|
+
if output_path.nil? && format_opt == :png
|
|
65
|
+
warn "*** Refusing to output PNG to STDOUT ***"
|
|
66
|
+
exit
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Raysetta::Runner.parse_scene(file_or_url,
|
|
70
|
+
output_path:,
|
|
71
|
+
runner: runner_opt,
|
|
72
|
+
format: format_opt,
|
|
73
|
+
concurrency:,
|
|
74
|
+
**options
|
|
75
|
+
)
|
data/lib/raysetta/hit.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
class Hit
|
|
5
|
+
attr_accessor :point, :normal, :t, :material, :uv
|
|
6
|
+
attr_writer :front_face
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
t:,
|
|
10
|
+
normal:,
|
|
11
|
+
material:,
|
|
12
|
+
r:,
|
|
13
|
+
point:,
|
|
14
|
+
uv: Vec2.new
|
|
15
|
+
)
|
|
16
|
+
@point = point
|
|
17
|
+
@t = t
|
|
18
|
+
@material = material
|
|
19
|
+
@uv = uv
|
|
20
|
+
@normal = normal
|
|
21
|
+
@front_face = true
|
|
22
|
+
|
|
23
|
+
set_face_normal(r) if r
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def front_face? = @front_face
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Sets the hit's normal vector.
|
|
31
|
+
# NOTE: `@normal` is assumed to have unit length.
|
|
32
|
+
def set_face_normal(r)
|
|
33
|
+
@front_face = r.direction.dot(@normal) < 0
|
|
34
|
+
@normal = @front_face ? @normal : -@normal
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chunky_png'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
class Image < EntityBase
|
|
7
|
+
attr_reader :image
|
|
8
|
+
|
|
9
|
+
def initialize(path: nil, data_url: nil)
|
|
10
|
+
@image = if path
|
|
11
|
+
ChunkyPNG::Image.from_file(path)
|
|
12
|
+
elsif data_url
|
|
13
|
+
ChunkyPNG::Image.from_data_url(data_url)
|
|
14
|
+
else
|
|
15
|
+
raise ArgumentError, "missing path or data_url"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def width = image.width
|
|
20
|
+
def height = image.height
|
|
21
|
+
|
|
22
|
+
def [](x, y)
|
|
23
|
+
pixel = image.get_pixel(x.clamp(0, width-1), y.clamp(0, height-1))
|
|
24
|
+
Vec3.new(ChunkyPNG::Color.r(pixel)/255.0, ChunkyPNG::Color.g(pixel)/255.0, ChunkyPNG::Color.b(pixel)/255.0).tap do |c|
|
|
25
|
+
c.r = Util.gamma_to_linear(c.r)
|
|
26
|
+
c.g = Util.gamma_to_linear(c.g)
|
|
27
|
+
c.b = Util.gamma_to_linear(c.b)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(img)
|
|
32
|
+
image == img.image
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
[type, image].hash
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def export
|
|
40
|
+
{
|
|
41
|
+
**super,
|
|
42
|
+
data: image.to_data_url
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|