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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
class Interval
|
|
5
|
+
attr_accessor :min, :max
|
|
6
|
+
|
|
7
|
+
# Default interval is empty
|
|
8
|
+
def initialize(min = Float::INFINITY, max = -Float::INFINITY)
|
|
9
|
+
@min = min
|
|
10
|
+
@max = max
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_range(r)
|
|
14
|
+
new(r.min, r.max)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create the interval tightly enclosing the two input intervals.
|
|
18
|
+
def self.from_intervals(a, b)
|
|
19
|
+
new(
|
|
20
|
+
a.min <= b.min ? a.min : b.min,
|
|
21
|
+
a.max >= b.max ? a.max : b.max
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def size = max - min
|
|
26
|
+
def include?(x) = min <= x && x <= max
|
|
27
|
+
def surround?(x) = min < x && x < max
|
|
28
|
+
|
|
29
|
+
def clamp(x)
|
|
30
|
+
return min if x < min
|
|
31
|
+
return max if x > max
|
|
32
|
+
x
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def expand(delta)
|
|
36
|
+
padding = delta / 2.0
|
|
37
|
+
Interval.new(min - padding, max + padding)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def expand!(delta)
|
|
41
|
+
padding = delta / 2.0
|
|
42
|
+
self.min -= padding
|
|
43
|
+
self.max += padding
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def dup
|
|
48
|
+
Interval.new(min, max)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ==(i)
|
|
52
|
+
min == i.min && max == i.max
|
|
53
|
+
end
|
|
54
|
+
alias :eql? :==
|
|
55
|
+
|
|
56
|
+
def hash
|
|
57
|
+
[min, max].hash
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
EMPTY = new(Float::INFINITY, -Float::INFINITY)
|
|
61
|
+
UNIVERSE = new(-Float::INFINITY, Float::INFINITY)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
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 = Vec3.new(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, r_in.time)
|
|
30
|
+
|
|
31
|
+
Scatter.new(scattered, attenuation)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def export
|
|
35
|
+
{
|
|
36
|
+
**super,
|
|
37
|
+
refraction_index: refraction_index
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ==(mat)
|
|
42
|
+
return false unless mat.is_a?(Dielectric)
|
|
43
|
+
|
|
44
|
+
refraction_index == mat.refraction_index
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def hash
|
|
48
|
+
[type, refraction_index].hash
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def reflectance(cosine, ri)
|
|
54
|
+
# Use Schlick's approximation for reflectance.
|
|
55
|
+
r0 = ((1.0 - ri) / (1.0 + ri))**2
|
|
56
|
+
r0 + (1.0-r0)*(1.0-cosine)**5
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Material
|
|
5
|
+
class DiffuseLight < Base
|
|
6
|
+
attr_accessor :texture
|
|
7
|
+
|
|
8
|
+
def initialize(texture)
|
|
9
|
+
@texture = texture
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.solid(albedo)
|
|
13
|
+
new(Texture::SolidColor.new(albedo))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def emitted(uv, point)
|
|
17
|
+
texture.sample(uv, point)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(mat)
|
|
21
|
+
return false unless mat.is_a?(DiffuseLight)
|
|
22
|
+
|
|
23
|
+
texture == mat.texture
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def hash
|
|
27
|
+
[type, texture].hash
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def export
|
|
31
|
+
{
|
|
32
|
+
**super,
|
|
33
|
+
texture: texture.id,
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def textures
|
|
38
|
+
[texture]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Material
|
|
5
|
+
class Lambertian < Base
|
|
6
|
+
attr_accessor :texture
|
|
7
|
+
|
|
8
|
+
def initialize(texture)
|
|
9
|
+
@texture = texture
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.solid(albedo)
|
|
13
|
+
new(Texture::SolidColor.new(albedo))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def scatter(r_in, rec)
|
|
17
|
+
scatter_direction = rec.normal + Vec3.random_unit
|
|
18
|
+
|
|
19
|
+
# Catch degenerate scatter direction
|
|
20
|
+
scatter_direction = rec.normal if scatter_direction.zero?
|
|
21
|
+
|
|
22
|
+
scattered = Ray.new(rec.point, scatter_direction, r_in.time)
|
|
23
|
+
Scatter.new(scattered, texture.sample(rec.uv, rec.point))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(mat)
|
|
27
|
+
return false unless mat.is_a?(Lambertian)
|
|
28
|
+
|
|
29
|
+
texture == mat.texture
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def hash
|
|
33
|
+
[type, texture].hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def export
|
|
37
|
+
{
|
|
38
|
+
**super,
|
|
39
|
+
texture: texture.id,
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def textures
|
|
44
|
+
[texture]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Material
|
|
5
|
+
class Metal < Base
|
|
6
|
+
attr_accessor :texture, :fuzz
|
|
7
|
+
|
|
8
|
+
def initialize(texture, fuzz)
|
|
9
|
+
@texture = texture
|
|
10
|
+
@fuzz = fuzz
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.solid(albedo, fuzz)
|
|
14
|
+
new(Texture::SolidColor.new(albedo), fuzz)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scatter(r_in, rec)
|
|
18
|
+
reflected = r_in.direction.reflect(rec.normal)
|
|
19
|
+
reflected = reflected.normalize.add(Vec3.random_unit.mul(fuzz))
|
|
20
|
+
scattered = Ray.new(rec.point, reflected, r_in.time)
|
|
21
|
+
return nil if scattered.direction.dot(rec.normal) <= 0.0
|
|
22
|
+
|
|
23
|
+
Scatter.new(scattered, texture.sample(rec.uv, rec.point))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(mat)
|
|
27
|
+
return false unless mat.is_a?(Metal)
|
|
28
|
+
|
|
29
|
+
texture == mat.texture && fuzz == mat.fuzz
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def hash
|
|
33
|
+
[type, texture, fuzz].hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def export
|
|
37
|
+
{
|
|
38
|
+
**super,
|
|
39
|
+
texture: texture.id,
|
|
40
|
+
fuzz: fuzz,
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def textures
|
|
45
|
+
[texture]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Base < EntityBase
|
|
6
|
+
def hit(r, ray_t)
|
|
7
|
+
raise NotImplementedError, "Implement #{self.class.name}#hit"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def bounding_box
|
|
11
|
+
raise NotImplementedError, "Implement #{self.class.name}#bounding_box"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def materials
|
|
15
|
+
[]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def objects
|
|
19
|
+
[self]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Box < Group
|
|
6
|
+
def initialize(a, b, material)
|
|
7
|
+
@a = a
|
|
8
|
+
@b = b
|
|
9
|
+
@material = material
|
|
10
|
+
|
|
11
|
+
# Returns the 3D box (six sides) that contains the two opposite vertices a & b.
|
|
12
|
+
|
|
13
|
+
# Construct the two opposite vertices with the minimum and maximum coordinates.
|
|
14
|
+
min = Vec3.new([a.x, b.x].min, [a.y, b.y].min, [a.z, b.z].min);
|
|
15
|
+
max = Vec3.new([a.x, b.x].max, [a.y, b.y].max, [a.z, b.z].max);
|
|
16
|
+
|
|
17
|
+
dx = Vec3.new(max.x - min.x, 0.0, 0.0)
|
|
18
|
+
dy = Vec3.new(0.0, max.y - min.y, 0.0)
|
|
19
|
+
dz = Vec3.new(0.0, 0.0, max.z - min.z)
|
|
20
|
+
|
|
21
|
+
super(
|
|
22
|
+
Quad.new(Vec3.new(min.x, min.y, max.z), dx, dy, material), # front
|
|
23
|
+
Quad.new(Vec3.new(max.x, min.y, max.z), -dz, dy, material), # right
|
|
24
|
+
Quad.new(Vec3.new(max.x, min.y, min.z), -dx, dy, material), # back
|
|
25
|
+
Quad.new(Vec3.new(min.x, min.y, min.z), dz, dy, material), # left
|
|
26
|
+
Quad.new(Vec3.new(min.x, max.y, max.z), dx, -dz, material), # top
|
|
27
|
+
Quad.new(Vec3.new(min.x, min.y, min.z), dx, dz, material) # bottom
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def export
|
|
32
|
+
{
|
|
33
|
+
type: "Box",
|
|
34
|
+
a: @a.to_a,
|
|
35
|
+
b: @b.to_a,
|
|
36
|
+
material: @material.id
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def materials
|
|
41
|
+
[@material]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_s(depth=0)
|
|
45
|
+
indent = " "*depth
|
|
46
|
+
"#{indent}Box { a=#{@a}, b=#{@b} }"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class BVH < Base
|
|
6
|
+
attr_reader :bounding_box
|
|
7
|
+
|
|
8
|
+
def initialize(objects)
|
|
9
|
+
self.objects = objects
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def hit(r, ray_t)
|
|
13
|
+
return nil unless bounding_box.hit(r, ray_t)
|
|
14
|
+
|
|
15
|
+
hit_left = @left.hit(r, ray_t)
|
|
16
|
+
hit_right = @right.hit(r, ray_t.min..(hit_left&.t || ray_t.max))
|
|
17
|
+
|
|
18
|
+
hit_right || hit_left
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def export
|
|
22
|
+
{ **export_bvh(@left), **export_bvh(@right) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def materials
|
|
26
|
+
[*@left.materials, *@right.materials].uniq
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def objects
|
|
30
|
+
@left.objects | @right.objects
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def objects=(objs)
|
|
34
|
+
@bounding_box = AABB::EMPTY
|
|
35
|
+
objs.each do |object|
|
|
36
|
+
@bounding_box = AABB.from_aabbs(@bounding_box, object.bounding_box)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
axis = @bounding_box.longest_axis
|
|
40
|
+
|
|
41
|
+
case objs.length
|
|
42
|
+
when 1
|
|
43
|
+
@left = @right = objs[0]
|
|
44
|
+
when 2
|
|
45
|
+
@left = objs[0]
|
|
46
|
+
@right = objs[1]
|
|
47
|
+
else
|
|
48
|
+
objs.sort_by! { _1.bounding_box[axis].min }
|
|
49
|
+
mid = objs.length / 2
|
|
50
|
+
left_objs = objs[0...mid] #: Array[Object::Base]
|
|
51
|
+
right_objs = objs[mid..] #: Array[Object::Base]
|
|
52
|
+
@left = BVH.new(left_objs)
|
|
53
|
+
@right = BVH.new(right_objs)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add(object)
|
|
58
|
+
self.objects = objects + [object]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s(depth=0)
|
|
62
|
+
indent = " "*depth
|
|
63
|
+
"#{indent}BVH {\n" +
|
|
64
|
+
"#{indent} left=\n"+
|
|
65
|
+
"#{@left.to_s(depth+2)}\n"+
|
|
66
|
+
"#{indent} right=\n"+
|
|
67
|
+
"#{@right.to_s(depth+2)}\n"+
|
|
68
|
+
"#{indent}}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def export_bvh(obj)
|
|
74
|
+
case obj
|
|
75
|
+
when BVH then obj.export
|
|
76
|
+
else { obj.id => obj.export }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Group < Base
|
|
6
|
+
attr_reader :objects, :bounding_box
|
|
7
|
+
|
|
8
|
+
def initialize(*objects)
|
|
9
|
+
self.objects = objects
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def objects=(objs)
|
|
13
|
+
@objects = objs
|
|
14
|
+
@bounding_box = AABB.from_aabbs(*objs.map(&:bounding_box))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add(obj)
|
|
18
|
+
@objects << obj
|
|
19
|
+
@bounding_box = AABB.from_aabbs(@bounding_box, obj.bounding_box)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def hit(r, ray_t)
|
|
23
|
+
hit = nil #: Hit?
|
|
24
|
+
closest_so_far = ray_t.max
|
|
25
|
+
|
|
26
|
+
@objects.each do |object|
|
|
27
|
+
if tmp = object.hit(r, ray_t.min..closest_so_far)
|
|
28
|
+
hit = tmp
|
|
29
|
+
# @type var hit: Hit
|
|
30
|
+
closest_so_far = hit.t
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
hit
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def export
|
|
38
|
+
{
|
|
39
|
+
**super,
|
|
40
|
+
objects: objects.map { [_1.id, _1.export] }.to_h
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def materials
|
|
45
|
+
@objects.flat_map(&:materials).uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_s(depth=0)
|
|
49
|
+
indent = " "*depth
|
|
50
|
+
"#{indent}Group {\n" +
|
|
51
|
+
"#{indent} objects = {" +
|
|
52
|
+
"#{@objects.map { _1.to_s(depth+2)+"\n" } }" +
|
|
53
|
+
"#{indent} }\n" +
|
|
54
|
+
"#{indent}}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class MovingSphere < Sphere
|
|
6
|
+
attr_accessor :center_vec
|
|
7
|
+
|
|
8
|
+
def initialize(center1, center2, radius, material)
|
|
9
|
+
super(center1, radius, material)
|
|
10
|
+
@center_vec = center2 - center1
|
|
11
|
+
rvec = Vec3.new(radius, radius, radius)
|
|
12
|
+
box1 = AABB.from_points(center1 - rvec, center1 + rvec)
|
|
13
|
+
box2 = AABB.from_points(center2 - rvec, center2 + rvec)
|
|
14
|
+
@bounding_box = AABB.from_aabbs(box1, box2)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Linearly interpolate from center1 to center2 according to time, where t=0 yields
|
|
18
|
+
# center1, and t=1 yields center2.
|
|
19
|
+
def center_at(time = 0.0)
|
|
20
|
+
center + center_vec * time
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def hit(r, ray_t, center = center_at(r.time))
|
|
24
|
+
super(r, ray_t, center)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def export
|
|
28
|
+
{
|
|
29
|
+
**super.except(:center),
|
|
30
|
+
center1: center.to_a,
|
|
31
|
+
center2: center_at(1.0).to_a
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s(depth=0)
|
|
36
|
+
indent = " "*depth
|
|
37
|
+
"#{indent}MovingSphere { radius=#{radius}, center=#{center}, vec=#{center_vec} }"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Quad < Base
|
|
6
|
+
attr_accessor :q, :u, :v, :material
|
|
7
|
+
|
|
8
|
+
attr_reader :bounding_box
|
|
9
|
+
|
|
10
|
+
def initialize(q, u, v, material)
|
|
11
|
+
@q = q
|
|
12
|
+
@u = u
|
|
13
|
+
@v = v
|
|
14
|
+
@material = material
|
|
15
|
+
|
|
16
|
+
n = u.cross(v)
|
|
17
|
+
@normal = n.unit
|
|
18
|
+
@d = @normal.dot(q)
|
|
19
|
+
@w = n / n.dot(n)
|
|
20
|
+
|
|
21
|
+
compute_bounding_box
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def hit(r, ray_t)
|
|
25
|
+
denom = normal.dot(r.direction)
|
|
26
|
+
|
|
27
|
+
# No hit if the ray is parallel to the plane.
|
|
28
|
+
return if denom.abs < 1e-8
|
|
29
|
+
|
|
30
|
+
# Return if the hit point parameter t is outside the ray interval.
|
|
31
|
+
t = (d - normal.dot(r.origin)) / denom
|
|
32
|
+
return unless ray_t.cover?(t)
|
|
33
|
+
|
|
34
|
+
# Determine the hit point lies within the planar shape using its plane coordinates.
|
|
35
|
+
intersection = r.at(t)
|
|
36
|
+
planar_hitpt_vector = intersection - q
|
|
37
|
+
alpha = w.dot(planar_hitpt_vector.cross(v))
|
|
38
|
+
beta = w.dot(u.cross(planar_hitpt_vector))
|
|
39
|
+
|
|
40
|
+
uv = interior?(alpha, beta)
|
|
41
|
+
return unless uv
|
|
42
|
+
|
|
43
|
+
# Ray hits the 2D shape
|
|
44
|
+
Hit.new(point: intersection, t:, normal:, material:, r:, uv:)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def export
|
|
48
|
+
{
|
|
49
|
+
**super,
|
|
50
|
+
q: q.to_a,
|
|
51
|
+
u: u.to_a,
|
|
52
|
+
v: v.to_a,
|
|
53
|
+
material: material.id
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def materials
|
|
58
|
+
[material]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s(depth=0)
|
|
62
|
+
indent = " "*depth
|
|
63
|
+
"#{indent}Quad { q=#{q}, u=#{u}, v=#{v} }"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_accessor :normal, :d, :w
|
|
68
|
+
|
|
69
|
+
def interior?(a, b)
|
|
70
|
+
unit_interval = Interval.new(0.0, 1.0)
|
|
71
|
+
# Given the hit point in plane coordinates, return nil if it is outside the
|
|
72
|
+
# primitive, otherwise return the UV coordinates.
|
|
73
|
+
|
|
74
|
+
return unless unit_interval.include?(a) && unit_interval.include?(b)
|
|
75
|
+
|
|
76
|
+
Vec2.new(a, b)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def compute_bounding_box
|
|
80
|
+
# Compute the bounding box of all four vertices.
|
|
81
|
+
bbox_diagonal1 = AABB.from_points(q, q + u + v)
|
|
82
|
+
bbox_diagonal2 = AABB.from_points(q + u, q + v)
|
|
83
|
+
@bounding_box = AABB.from_aabbs(bbox_diagonal1, bbox_diagonal2)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raysetta
|
|
4
|
+
module Object
|
|
5
|
+
class Sphere < Base
|
|
6
|
+
attr_accessor :center, :radius, :material
|
|
7
|
+
|
|
8
|
+
attr_reader :bounding_box
|
|
9
|
+
|
|
10
|
+
def initialize(center, radius, material)
|
|
11
|
+
@center = center
|
|
12
|
+
@radius = radius
|
|
13
|
+
@material = material
|
|
14
|
+
rvec = Vec3.new(radius, radius, radius)
|
|
15
|
+
@bounding_box = AABB.from_points(center - rvec, center + rvec)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def hit(r, ray_t, center = @center)
|
|
19
|
+
oc = center - r.origin
|
|
20
|
+
a = r.direction.length_squared
|
|
21
|
+
h = r.direction.dot(oc)
|
|
22
|
+
c = oc.length_squared - radius**2
|
|
23
|
+
|
|
24
|
+
discriminant = h**2 - a*c
|
|
25
|
+
return nil if discriminant < 0
|
|
26
|
+
|
|
27
|
+
sqrtd = Math.sqrt(discriminant)
|
|
28
|
+
|
|
29
|
+
# Find the nearest root that lies in the acceptable range.
|
|
30
|
+
root = (h - sqrtd) / a
|
|
31
|
+
unless ray_t.cover?(root)
|
|
32
|
+
root = (h + sqrtd) / a
|
|
33
|
+
return nil unless ray_t.cover?(root)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
point = r.at(root)
|
|
37
|
+
normal = (point - center).div(radius)
|
|
38
|
+
|
|
39
|
+
Hit.new(
|
|
40
|
+
point:,
|
|
41
|
+
r:,
|
|
42
|
+
normal:,
|
|
43
|
+
t: root,
|
|
44
|
+
material:,
|
|
45
|
+
uv: Util.sphere_uv(normal)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def export
|
|
50
|
+
{
|
|
51
|
+
**super,
|
|
52
|
+
center: center.to_a,
|
|
53
|
+
radius: radius,
|
|
54
|
+
material: material.id
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def materials
|
|
59
|
+
[material]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s(depth=0)
|
|
63
|
+
indent = " "*depth
|
|
64
|
+
"#{indent}Sphere { radius=#{radius}, center=#{center} }"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|