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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Tri < Quad
|
|
6
|
+
def initialize(a, b, c, material)
|
|
7
|
+
super(a, b - a, c - a, material)
|
|
8
|
+
# TODO: uv coordinates
|
|
9
|
+
# TODO: front face based on clockwizeness of points
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def export
|
|
13
|
+
{
|
|
14
|
+
**super,
|
|
15
|
+
a: q.to_a,
|
|
16
|
+
b: (q + u).to_a,
|
|
17
|
+
c: (q + v).to_a,
|
|
18
|
+
material: material.id
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s(depth=0)
|
|
23
|
+
indent = " "*depth
|
|
24
|
+
"#{indent}Triangle { a=#{q}, b=#{q + u}, c=#{q + v} }"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def interior?(a, b)
|
|
30
|
+
# Given the hit point in plane coordinates, return nil if it is outside the
|
|
31
|
+
# primitive, otherwise return the UV coordinates.
|
|
32
|
+
|
|
33
|
+
return unless a > 0.0 && b > 0.0 && a + b < 1.0
|
|
34
|
+
|
|
35
|
+
# TODO: uv coordinates
|
|
36
|
+
Vec2.new(a, b)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def compute_bounding_box
|
|
40
|
+
# Compute the bounding box of all three vertices.
|
|
41
|
+
@bounding_box = AABB.from_points(q, q + u, q + v);
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Output
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :pixels, :width, :height
|
|
7
|
+
|
|
8
|
+
def initialize(pixels, width:, height:)
|
|
9
|
+
@pixels = pixels
|
|
10
|
+
@width = width
|
|
11
|
+
@height = height
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
raise NotImplementedError, "Implement #{self.class}#call"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def save(file)
|
|
19
|
+
File.write(file, call)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chunky_png'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
module Output
|
|
7
|
+
class PNG < Base
|
|
8
|
+
def call
|
|
9
|
+
image = ChunkyPNG::Image.new(width, height)
|
|
10
|
+
|
|
11
|
+
pixels.each.with_index do |row, y|
|
|
12
|
+
row.each.with_index do |pixel, x|
|
|
13
|
+
image.set_pixel(x, y, ChunkyPNG::Color.rgb(pixel[0], pixel[1], pixel[2]))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
image
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def save(file)
|
|
21
|
+
image = call
|
|
22
|
+
image.save(file, :fast_rgb)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Output
|
|
5
|
+
class PPM < Base
|
|
6
|
+
def call
|
|
7
|
+
buffer = "P3\n#{width} #{height}\n255\n"
|
|
8
|
+
|
|
9
|
+
pixels.each do |row|
|
|
10
|
+
row.each do |pixel|
|
|
11
|
+
buffer << pixel.join(' ') << "\n"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
buffer
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
class Perlin < EntityBase
|
|
5
|
+
POINT_COUNT = 256
|
|
6
|
+
|
|
7
|
+
attr_accessor :randvec, :perm_x, :perm_y, :perm_z
|
|
8
|
+
|
|
9
|
+
def initialize(
|
|
10
|
+
randvec = Array.new(POINT_COUNT) { Vec3.random(-1.0, 1.0).normalize },
|
|
11
|
+
perm_x = (0...POINT_COUNT).to_a.shuffle!,
|
|
12
|
+
perm_y = (0...POINT_COUNT).to_a.shuffle!,
|
|
13
|
+
perm_z = (0...POINT_COUNT).to_a.shuffle!
|
|
14
|
+
)
|
|
15
|
+
@randvec = randvec
|
|
16
|
+
@perm_x = perm_x
|
|
17
|
+
@perm_y = perm_y
|
|
18
|
+
@perm_z = perm_z
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def noise(point)
|
|
22
|
+
ivec = point.to_a.map(&:floor) #: [Integer, Integer, Integer]
|
|
23
|
+
i = ivec[0]
|
|
24
|
+
j = ivec[1]
|
|
25
|
+
k = ivec[2]
|
|
26
|
+
vec = point - Vec3.new(i.to_f, j.to_f, k.to_f)
|
|
27
|
+
|
|
28
|
+
c = Array.new(2) { Array.new(2) { Array.new(2) } }
|
|
29
|
+
|
|
30
|
+
CUBE.each do |di, dj, dk|
|
|
31
|
+
c[di][dj][dk] = @randvec[
|
|
32
|
+
@perm_x[(i + di) & 255] ^
|
|
33
|
+
@perm_y[(j + dj) & 255] ^
|
|
34
|
+
@perm_z[(k + dk) & 255]
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Perlin.perlin_interp(c, vec)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def turb(point, depth)
|
|
42
|
+
accum = 0.0
|
|
43
|
+
temp_p = point
|
|
44
|
+
weight = 1.0
|
|
45
|
+
|
|
46
|
+
(0...depth).each do
|
|
47
|
+
accum += weight * noise(temp_p)
|
|
48
|
+
weight *= 0.5
|
|
49
|
+
temp_p *= 2.0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
accum.abs
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(perlin)
|
|
56
|
+
randvec == perlin.randvec &&
|
|
57
|
+
perm_x == perlin.perm_x &&
|
|
58
|
+
perm_y == perlin.perm_y &&
|
|
59
|
+
perm_z == perlin.perm_z
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias :eql? :==
|
|
63
|
+
|
|
64
|
+
def hash
|
|
65
|
+
[randvec, perm_x, perm_y, perm_z].hash
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def export
|
|
69
|
+
{
|
|
70
|
+
**super,
|
|
71
|
+
randvec: randvec.map(&:to_a),
|
|
72
|
+
perm_x: perm_x,
|
|
73
|
+
perm_y: perm_y,
|
|
74
|
+
perm_z: perm_z,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.perlin_interp(c, vec)
|
|
79
|
+
v = vec.smoothstep
|
|
80
|
+
CUBE.sum do |i, j, k|
|
|
81
|
+
weight = vec - Vec3.new(i.to_f, j.to_f, k.to_f)
|
|
82
|
+
(i * v.x + (1 - i) * (1 - v.x)) *
|
|
83
|
+
(j * v.y + (1 - j) * (1 - v.y)) *
|
|
84
|
+
(k * v.z + (1 - k) * (1 - v.z)) *
|
|
85
|
+
c[i][j][k].dot(weight)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
CUBE = [0, 1].product([0, 1],[0, 1])
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/raysetta/ray.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
class Ray
|
|
5
|
+
attr_accessor :origin, :direction, :time
|
|
6
|
+
|
|
7
|
+
def initialize(origin, direction, time = 0.0)
|
|
8
|
+
@origin = origin
|
|
9
|
+
@direction = direction
|
|
10
|
+
@time = time
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def at(t) = (direction*t).add(origin)
|
|
14
|
+
|
|
15
|
+
def dup = self.class.new(origin.dup, direction.dup, time)
|
|
16
|
+
|
|
17
|
+
def ==(r)
|
|
18
|
+
origin == r.origin &&
|
|
19
|
+
direction == r.direction &&
|
|
20
|
+
(time- r.time).abs < Util::EPSILON
|
|
21
|
+
end
|
|
22
|
+
alias :eql? :==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
[origin, direction, time].hash
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'progress'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
module Runner
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :tracer
|
|
9
|
+
|
|
10
|
+
def initialize(tracer)
|
|
11
|
+
@tracer = tracer
|
|
12
|
+
Progress.start("Tracing", tracer.height)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
raise NotImplementedError, "Implement #{self.class}#call"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def progress
|
|
20
|
+
Progress.step
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def finish
|
|
24
|
+
Progress.stop
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parallel'
|
|
4
|
+
require 'etc'
|
|
5
|
+
|
|
6
|
+
module Raysetta
|
|
7
|
+
module Runner
|
|
8
|
+
class Concurrent < Base
|
|
9
|
+
attr_reader :count, :output
|
|
10
|
+
|
|
11
|
+
def initialize(tracer, count: Etc.nprocessors)
|
|
12
|
+
super(tracer)
|
|
13
|
+
@count = count
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parallel'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
module Runner
|
|
7
|
+
class Processes < Concurrent
|
|
8
|
+
attr_reader :output
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
@output = Parallel.map(tracer.height.times.to_a, in_processes: count, finish: proc { progress }) do |y|
|
|
12
|
+
row = Array.new(tracer.width) { [0, 0, 0] } #: Array[[Integer, Integer, Integer]]
|
|
13
|
+
row.each.with_index do |pixel, x|
|
|
14
|
+
r, g, b = tracer.call(x, y)
|
|
15
|
+
|
|
16
|
+
pixel[0] = r
|
|
17
|
+
pixel[1] = g
|
|
18
|
+
pixel[2] = b
|
|
19
|
+
end
|
|
20
|
+
row
|
|
21
|
+
end
|
|
22
|
+
finish
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parallel'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
module Runner
|
|
7
|
+
class Ractors < Concurrent
|
|
8
|
+
attr_reader :output
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
Ractor.make_shareable(tracer)
|
|
12
|
+
@output = Parallel.map(tracer.height.times.map { [_1, tracer] }, in_ractors: count, ractor: [self.class, :run], finish: proc { progress })
|
|
13
|
+
finish
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.run(args)
|
|
17
|
+
y = args[0] #: Integer
|
|
18
|
+
tracer = args[1] #: Tracer
|
|
19
|
+
|
|
20
|
+
row = Array.new(tracer.width) { [0, 0, 0] } #: Array[[Integer, Integer, Integer]]
|
|
21
|
+
row.each.with_index do |pixel, x|
|
|
22
|
+
r, g, b = tracer.call(x, y)
|
|
23
|
+
|
|
24
|
+
pixel[0] = r
|
|
25
|
+
pixel[1] = g
|
|
26
|
+
pixel[2] = b
|
|
27
|
+
end
|
|
28
|
+
row
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Runner
|
|
5
|
+
class Sync < Base
|
|
6
|
+
attr_reader :output
|
|
7
|
+
|
|
8
|
+
def initialize(tracer)
|
|
9
|
+
super
|
|
10
|
+
@output = Array.new(tracer.height) { Array.new(tracer.width) { [0, 0, 0] } }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
@output.each.with_index do |row, i|
|
|
15
|
+
row.each.with_index do |pixel, j|
|
|
16
|
+
r, g, b = tracer.call(j, i)
|
|
17
|
+
|
|
18
|
+
pixel[0] = r
|
|
19
|
+
pixel[1] = g
|
|
20
|
+
pixel[2] = b
|
|
21
|
+
end
|
|
22
|
+
progress
|
|
23
|
+
end
|
|
24
|
+
finish
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parallel'
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
module Runner
|
|
7
|
+
class Threads < Concurrent
|
|
8
|
+
attr_reader :output
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
@output = Parallel.map(tracer.height.times.to_a, in_threads: count, finish: proc { progress }) do |y|
|
|
12
|
+
row = Array.new(tracer.width) { [0, 0, 0] } #: Array[[Integer, Integer, Integer]]
|
|
13
|
+
row.each.with_index do |pixel, x|
|
|
14
|
+
r, g, b = tracer.call(x, y)
|
|
15
|
+
|
|
16
|
+
pixel[0] = r
|
|
17
|
+
pixel[1] = g
|
|
18
|
+
pixel[2] = b
|
|
19
|
+
end
|
|
20
|
+
row
|
|
21
|
+
end
|
|
22
|
+
finish
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
|
|
5
|
+
require_relative "runner/base"
|
|
6
|
+
require_relative "runner/concurrent"
|
|
7
|
+
require_relative "runner/sync"
|
|
8
|
+
require_relative "runner/threads"
|
|
9
|
+
require_relative "runner/ractors"
|
|
10
|
+
require_relative "runner/processes"
|
|
11
|
+
require_relative "output/base"
|
|
12
|
+
require_relative "output/ppm"
|
|
13
|
+
require_relative "output/png"
|
|
14
|
+
|
|
15
|
+
module Raysetta
|
|
16
|
+
module Runner
|
|
17
|
+
def self.parse_scene(input_path, output_path: nil, runner: :sync, format: :ppm, concurrency: Etc.nprocessors, **options)
|
|
18
|
+
scene = Raysetta::Scene.parse(JSON.parse(File.read(input_path)))
|
|
19
|
+
|
|
20
|
+
tracer = Raysetta::Tracer.new(scene, **options)
|
|
21
|
+
runner = case runner
|
|
22
|
+
when :sync then Raysetta::Runner::Sync.new(tracer)
|
|
23
|
+
when :threads then Raysetta::Runner::Threads.new(tracer, count: concurrency)
|
|
24
|
+
when :ractors then Raysetta::Runner::Ractors.new(tracer, count: concurrency)
|
|
25
|
+
when :processes then Raysetta::Runner::Processes.new(tracer, count: concurrency)
|
|
26
|
+
else
|
|
27
|
+
warn "*** Unknown runner #{runner} ***"
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
runner.call
|
|
31
|
+
out = case format
|
|
32
|
+
when :ppm then Raysetta::Output::PPM.new(runner.output, width: tracer.width, height: tracer.height)
|
|
33
|
+
when :png then Raysetta::Output::PNG.new(runner.output, width: tracer.width, height: tracer.height)
|
|
34
|
+
else
|
|
35
|
+
warn "*** Unknown format #{format} ***"
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
if output_path
|
|
39
|
+
out.save(output_path)
|
|
40
|
+
else
|
|
41
|
+
puts out.call
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Raysetta
|
|
6
|
+
class Scene
|
|
7
|
+
attr_accessor :world, :camera, :background
|
|
8
|
+
|
|
9
|
+
def initialize(world, camera, background)
|
|
10
|
+
@world = world
|
|
11
|
+
@camera = camera
|
|
12
|
+
@background = background
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.parse(json)
|
|
16
|
+
Parser.new(json).parse
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def export
|
|
20
|
+
all_materials = world.materials
|
|
21
|
+
all_textures = [*all_materials, background].flat_map(&:textures).uniq
|
|
22
|
+
all_images = all_textures.flat_map(&:images).uniq
|
|
23
|
+
all_noises = all_textures.flat_map(&:noises).uniq
|
|
24
|
+
{
|
|
25
|
+
world: world.export,
|
|
26
|
+
camera: camera.export,
|
|
27
|
+
background: background.export,
|
|
28
|
+
materials: export_by_id(all_materials),
|
|
29
|
+
textures: export_by_id(all_textures),
|
|
30
|
+
images: export_by_id(all_images),
|
|
31
|
+
noises: export_by_id(all_noises)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def export_by_id(entities)
|
|
38
|
+
exported = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
39
|
+
entities.each do |entity|
|
|
40
|
+
exported[entity.id] = entity.export
|
|
41
|
+
end
|
|
42
|
+
exported
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Parser
|
|
46
|
+
class Error < Raysetta::Error; end
|
|
47
|
+
|
|
48
|
+
attr_reader :json
|
|
49
|
+
|
|
50
|
+
def initialize(json)
|
|
51
|
+
@json = json
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse
|
|
55
|
+
@noises = json["noises"].transform_values { parse_noise(_1) }
|
|
56
|
+
@images = json["images"].transform_values { parse_image(_1) }
|
|
57
|
+
@textures = json["textures"].transform_values { parse_texture(_1) }
|
|
58
|
+
@materials = json["materials"].transform_values { parse_material(_1) }
|
|
59
|
+
world = parse_world(json["world"])
|
|
60
|
+
camera = parse_camera(json["camera"])
|
|
61
|
+
background = parse_background(json["background"])
|
|
62
|
+
|
|
63
|
+
Scene.new(world, camera, background)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_noise(noise)
|
|
67
|
+
Perlin.new(
|
|
68
|
+
noise["randvec"].map { |v| vec3(v) },
|
|
69
|
+
noise["perm_x"],
|
|
70
|
+
noise["perm_y"],
|
|
71
|
+
noise["perm_z"]
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_image(img)
|
|
76
|
+
Image.new(data_url: img["data"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_texture(tex)
|
|
80
|
+
case tex["type"]
|
|
81
|
+
when "SolidColor" then Texture::SolidColor.new(rgb(tex["albedo"]))
|
|
82
|
+
when "Checker" then Texture::Checker.new(tex["scale"], parse_texture(tex["even"]), parse_texture(tex["odd"]))
|
|
83
|
+
when "Image" then Texture::Image.new(image(tex["image"]))
|
|
84
|
+
when "Noise" then Texture::Noise.new(tex["scale"], tex["depth"], tex["marble_axis"]&.to_sym, noise(tex["noise"]))
|
|
85
|
+
else raise Error, "unknown texture type #{tex["type"]}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_material(mat)
|
|
90
|
+
case mat["type"]
|
|
91
|
+
when "Lambertian" then Material::Lambertian.new(texture(mat["texture"]))
|
|
92
|
+
when "Metal" then Material::Metal.new(texture(mat["texture"]), mat["fuzz"])
|
|
93
|
+
when "Dielectric" then Material::Dielectric.new(mat["refraction_index"])
|
|
94
|
+
when "DiffuseLight" then Material::DiffuseLight.new(texture(mat["texture"]))
|
|
95
|
+
else raise Error, "unknown material type #{mat["type"]}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_world(json)
|
|
100
|
+
Object::BVH.new(json.map { |_, v| parse_object(v) })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_object(obj)
|
|
104
|
+
case obj["type"]
|
|
105
|
+
when "Sphere" then parse_sphere(obj)
|
|
106
|
+
when "MovingSphere" then parse_moving_sphere(obj)
|
|
107
|
+
when "Quad" then parse_quad(obj)
|
|
108
|
+
when "Tri" then parse_tri(obj)
|
|
109
|
+
when "Box" then parse_box(obj)
|
|
110
|
+
else raise Error, "unknown object type #{obj["type"]}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_sphere(obj)
|
|
115
|
+
Object::Sphere.new(vec3(obj["center"]), obj["radius"], material(obj["material"]))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_moving_sphere(obj)
|
|
119
|
+
Object::MovingSphere.new(vec3(obj["center1"]), vec3(obj["center2"]), obj["radius"], material(obj["material"]))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_quad(obj)
|
|
123
|
+
Object::Quad.new(vec3(obj["q"]), vec3(obj["u"]), vec3(obj["v"]), material(obj["material"]))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_tri(obj)
|
|
127
|
+
Object::Tri.new(vec3(obj["a"]), vec3(obj["b"]), vec3(obj["c"]), material(obj["material"]))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_box(obj)
|
|
131
|
+
Object::Box.new(vec3(obj["a"]), vec3(obj["b"]), material(obj["material"]))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def material(name)
|
|
135
|
+
raise Error, "unknown material #{name}" unless @materials.key?(name)
|
|
136
|
+
|
|
137
|
+
@materials[name]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def texture(name)
|
|
141
|
+
raise Error, "unknown texture #{name}" unless @textures.key?(name)
|
|
142
|
+
|
|
143
|
+
@textures[name]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def image(name)
|
|
147
|
+
raise Error, "unknown image #{name}" unless @images.key?(name)
|
|
148
|
+
|
|
149
|
+
@images[name]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def noise(name)
|
|
153
|
+
raise Error, "unknown noise #{name}" unless @noises.key?(name)
|
|
154
|
+
|
|
155
|
+
@noises[name]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_camera(cam)
|
|
159
|
+
args = {} #: Hash[Symbol, untyped]
|
|
160
|
+
args[:vfov] = cam["vfov"] if cam.key?("vfov")
|
|
161
|
+
args[:lookfrom] = vec3(cam["lookfrom"]) if cam.key?("lookfrom")
|
|
162
|
+
args[:lookat] = vec3(cam["lookat"]) if cam.key?("lookat")
|
|
163
|
+
args[:vup] = vec3(cam["vup"]) if cam.key?("vup")
|
|
164
|
+
args[:defocus_angle] = cam["defocus_angle"] if cam.key?("defocus_angle")
|
|
165
|
+
args[:focus_dist] = cam["focus_dist"] if cam.key?("focus_dist")
|
|
166
|
+
Camera.new(**args)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_background(bg)
|
|
170
|
+
case bg["type"]
|
|
171
|
+
when "Solid" then parse_solid_bg(bg)
|
|
172
|
+
when "Gradient" then parse_gradient_bg(bg)
|
|
173
|
+
when "SphereMap" then parse_sphere_map(bg)
|
|
174
|
+
when "CubeMap" then parse_cube_map(bg)
|
|
175
|
+
else raise Error, "unknown background type #{bg["type"]}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def parse_solid_bg(bg)
|
|
180
|
+
Background::Solid.new(rgb(bg["albedo"]))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_gradient_bg(bg)
|
|
184
|
+
Background::Gradient.new(rgb(bg["top"]), rgb(bg["bottom"]))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def parse_sphere_map(bg)
|
|
188
|
+
Background::SphereMap.new(texture(bg["texture"]))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def parse_cube_map(bg)
|
|
192
|
+
Background::CubeMap.new(*bg["textures"].map { texture(_1) })
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def vec3(e)
|
|
196
|
+
Vec3.new(e[0], e[1], e[2])
|
|
197
|
+
end
|
|
198
|
+
alias :rgb :vec3
|
|
199
|
+
|
|
200
|
+
def vec2(e)
|
|
201
|
+
Vec2.new(e[0], e[1])
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Texture
|
|
5
|
+
class Base < EntityBase
|
|
6
|
+
def sample(uv, point)
|
|
7
|
+
raise NotImplementedError, "Implement #{self.class.name}#sample"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def images
|
|
11
|
+
[]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def noises
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|