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,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
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Raysetta
4
+ class Scatter
5
+ attr_accessor :ray, :attenuation
6
+
7
+ def initialize(ray, attenuation)
8
+ @ray = ray
9
+ @attenuation = attenuation
10
+ end
11
+ end
12
+ 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