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.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +72 -0
  4. data/Rakefile +8 -0
  5. data/Steepfile +15 -0
  6. data/exe/raysetta +3 -0
  7. data/lib/raysetta/aabb.rb +116 -0
  8. data/lib/raysetta/background/base.rb +19 -0
  9. data/lib/raysetta/background/cube_map.rb +44 -0
  10. data/lib/raysetta/background/gradient.rb +28 -0
  11. data/lib/raysetta/background/solid.rb +24 -0
  12. data/lib/raysetta/background/sphere_map.rb +30 -0
  13. data/lib/raysetta/camera.rb +109 -0
  14. data/lib/raysetta/entity_base.rb +28 -0
  15. data/lib/raysetta/exe.rb +75 -0
  16. data/lib/raysetta/hit.rb +38 -0
  17. data/lib/raysetta/image.rb +46 -0
  18. data/lib/raysetta/interval.rb +63 -0
  19. data/lib/raysetta/material/base.rb +19 -0
  20. data/lib/raysetta/material/dielectric.rb +60 -0
  21. data/lib/raysetta/material/diffuse_light.rb +42 -0
  22. data/lib/raysetta/material/lambertian.rb +48 -0
  23. data/lib/raysetta/material/metal.rb +49 -0
  24. data/lib/raysetta/object/base.rb +23 -0
  25. data/lib/raysetta/object/box.rb +50 -0
  26. data/lib/raysetta/object/bvh.rb +81 -0
  27. data/lib/raysetta/object/group.rb +59 -0
  28. data/lib/raysetta/object/moving_sphere.rb +41 -0
  29. data/lib/raysetta/object/quad.rb +88 -0
  30. data/lib/raysetta/object/sphere.rb +68 -0
  31. data/lib/raysetta/object/tri.rb +46 -0
  32. data/lib/raysetta/output/base.rb +23 -0
  33. data/lib/raysetta/output/png.rb +26 -0
  34. data/lib/raysetta/output/ppm.rb +19 -0
  35. data/lib/raysetta/perlin.rb +91 -0
  36. data/lib/raysetta/ray.rb +28 -0
  37. data/lib/raysetta/runner/base.rb +28 -0
  38. data/lib/raysetta/runner/concurrent.rb +17 -0
  39. data/lib/raysetta/runner/processes.rb +26 -0
  40. data/lib/raysetta/runner/ractors.rb +32 -0
  41. data/lib/raysetta/runner/sync.rb +28 -0
  42. data/lib/raysetta/runner/threads.rb +26 -0
  43. data/lib/raysetta/runner.rb +45 -0
  44. data/lib/raysetta/scatter.rb +12 -0
  45. data/lib/raysetta/scene.rb +205 -0
  46. data/lib/raysetta/texture/base.rb +19 -0
  47. data/lib/raysetta/texture/checker.rb +55 -0
  48. data/lib/raysetta/texture/image.rb +57 -0
  49. data/lib/raysetta/texture/noise.rb +46 -0
  50. data/lib/raysetta/texture/solid_color.rb +36 -0
  51. data/lib/raysetta/tracer.rb +43 -0
  52. data/lib/raysetta/util.rb +38 -0
  53. data/lib/raysetta/vec2.rb +125 -0
  54. data/lib/raysetta/vec3.rb +214 -0
  55. data/lib/raysetta/version.rb +5 -0
  56. data/lib/raysetta.rb +44 -0
  57. data/mise.toml +2 -0
  58. data/scenes/box.json +26 -0
  59. data/scenes/cornell.json +108 -0
  60. data/scenes/cubemap.json +109 -0
  61. data/scenes/dumb.json +58 -0
  62. data/scenes/earth.json +79 -0
  63. data/scenes/example.json +11712 -0
  64. data/scenes/perlin.json +2126 -0
  65. data/scenes/quads.json +38 -0
  66. data/scenes/simple_light.json +2126 -0
  67. data/scenes/testcube.json +113 -0
  68. data/sig/raysetta/aabb.rbs +39 -0
  69. data/sig/raysetta/background/base.rbs +11 -0
  70. data/sig/raysetta/background/cube_map.rbs +9 -0
  71. data/sig/raysetta/background/gradient.rbs +10 -0
  72. data/sig/raysetta/background/solid.rbs +9 -0
  73. data/sig/raysetta/background/sphere_map.rbs +9 -0
  74. data/sig/raysetta/camera.rbs +41 -0
  75. data/sig/raysetta/entity_base.rbs +12 -0
  76. data/sig/raysetta/hit.rbs +21 -0
  77. data/sig/raysetta/image.rbs +13 -0
  78. data/sig/raysetta/interval.rbs +36 -0
  79. data/sig/raysetta/material/base.rbs +11 -0
  80. data/sig/raysetta/material/dielectric.rbs +15 -0
  81. data/sig/raysetta/material/diffuse_light.rbs +12 -0
  82. data/sig/raysetta/material/lambertian.rbs +10 -0
  83. data/sig/raysetta/material/metal.rbs +11 -0
  84. data/sig/raysetta/object/base.rbs +15 -0
  85. data/sig/raysetta/object/box.rbs +13 -0
  86. data/sig/raysetta/object/bvh.rbs +23 -0
  87. data/sig/raysetta/object/group.rbs +16 -0
  88. data/sig/raysetta/object/moving_sphere.rbs +15 -0
  89. data/sig/raysetta/object/quad.rbs +26 -0
  90. data/sig/raysetta/object/sphere.rbs +14 -0
  91. data/sig/raysetta/object/tri.rbs +15 -0
  92. data/sig/raysetta/output/base.rbs +15 -0
  93. data/sig/raysetta/output/png.rbs +8 -0
  94. data/sig/raysetta/output/ppm.rbs +7 -0
  95. data/sig/raysetta/perlin.rbs +19 -0
  96. data/sig/raysetta/ray.rbs +18 -0
  97. data/sig/raysetta/runner/base.rbs +15 -0
  98. data/sig/raysetta/runner/concurrent.rbs +10 -0
  99. data/sig/raysetta/runner/processes.rbs +9 -0
  100. data/sig/raysetta/runner/ractors.rbs +11 -0
  101. data/sig/raysetta/runner/sync.rbs +11 -0
  102. data/sig/raysetta/runner/threads.rbs +9 -0
  103. data/sig/raysetta/runner.rbs +5 -0
  104. data/sig/raysetta/scatter.rbs +8 -0
  105. data/sig/raysetta/scene.rbs +69 -0
  106. data/sig/raysetta/texture/base.rbs +11 -0
  107. data/sig/raysetta/texture/checker.rbs +15 -0
  108. data/sig/raysetta/texture/image.rbs +11 -0
  109. data/sig/raysetta/texture/noise.rbs +13 -0
  110. data/sig/raysetta/texture/solid_color.rbs +10 -0
  111. data/sig/raysetta/tracer.rbs +16 -0
  112. data/sig/raysetta/util.rbs +15 -0
  113. data/sig/raysetta/vec2.rbs +48 -0
  114. data/sig/raysetta/vec3.rbs +67 -0
  115. data/sig/raysetta.rbs +6 -0
  116. data/sig/stdlib/chunky_png.rbs +24 -0
  117. data/sig/stdlib/etc.rbs +3 -0
  118. data/sig/stdlib/json.rbs +3 -0
  119. data/sig/stdlib/optparse.rbs +4 -0
  120. data/sig/stdlib/parallel.rbs +5 -0
  121. data/sig/stdlib/progress.rbs +5 -0
  122. 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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raysetta
4
+ module Material
5
+ class Base < EntityBase
6
+ def emitted(uv, point)
7
+ return Vec3.new
8
+ end
9
+
10
+ def scatter(r_in, rec)
11
+ nil
12
+ end
13
+
14
+ def textures
15
+ []
16
+ end
17
+ end
18
+ end
19
+ 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