ra 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3789cd972787fd603b8b9fb569a1c57ee339d908ccb3b28b27cc66631f9568ef
4
- data.tar.gz: 748652af8fde85dbeb80fbdfc2cf4fd33756938654522d9f3c2b03f739bcadad
3
+ metadata.gz: b56b6d752ce23d6484526702a0702d865bfad49f0209a40a8e597adf7a3c1b13
4
+ data.tar.gz: e45c8ddc28b1723f9e2a114386a2a0927026e9cf2cb54a38f438f3583d84710c
5
5
  SHA512:
6
- metadata.gz: 862fb845e4379218540100334fb7366f9ecf1674d1026da473ddc6c783bf228b6483512bf51d0c83218ec650ff1959e61a48756953da1a6118aae165c4057f77
7
- data.tar.gz: 9d21e31c6fda9b0795316ea2a9690fd087d646b5758ab873d82fb2aebec377cd08327a0eefde9cea00d8ff4e1088ddbb015691f7e16baee509b967918b07bcd6
6
+ metadata.gz: 0b2723e84ab6a64a4036615d298e733714838a754aecd59e0146eef97bd7b3c01374a3a9b4e145e9bb3ac437fe6499b41d819bf3fb79be92494bd2a2629c6f39
7
+ data.tar.gz: 1f74e78ddc9fbd2c61e231601d08519a8279279418ce8df0e8d6528a183412e09bbbfd138cbb3cd15fcb96979cac30b312520b3694b2c1232d394a1598dcfa57
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ra
2
2
 
3
- Named for ["Ra"](https://en.wikipedia.org/wiki/Ra).
3
+ Named for [Ra](https://en.wikipedia.org/wiki/Ra) - arguably the original ray tracer.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,7 +11,8 @@ gem install ra
11
11
  ## Usage
12
12
 
13
13
  ```sh
14
- ra -w 2560 -h 2048 | convert - sample.avif
14
+ ra -w 2560 -h 2048 > sample.ppm
15
+ convert -quality 80 sample.ppm sample.avif
15
16
  ```
16
17
 
17
18
  ![Sample](./sample.avif)
data/exe/ra CHANGED
@@ -23,9 +23,16 @@ config = Slop.parse(ARGV) do |options|
23
23
  end
24
24
  end
25
25
 
26
- light = Ra::Light.new(
26
+ earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'textures/earth.avif'))
27
+
28
+ light_l = Ra::Light.new(
27
29
  position: Vector[+5, +7, -9, Ra::Tuple::POINT],
28
- intensity: Ra::Color.white,
30
+ intensity: Ra::Color.uniform(0.4),
31
+ )
32
+
33
+ light_r = Ra::Light.new(
34
+ position: Vector[-5, +7, -9, Ra::Tuple::POINT],
35
+ intensity: Ra::Color.uniform(0.8),
29
36
  )
30
37
 
31
38
  camera = Ra::Camera.new(
@@ -39,33 +46,22 @@ camera = Ra::Camera.new(
39
46
  ),
40
47
  )
41
48
 
42
- floor_material = Ra::Material.new(base: Ra::Pattern::Checkers.new(
43
- colors: [
44
- Ra::Color.hex('#e2e8f0'),
45
- Ra::Color.hex('#94a3b8'),
46
- ],
47
- transform: Ra::Transform
48
- .scale(0.4, 0.4, 0.4)
49
- .translate(0, +0.2, 0),
50
- ))
51
-
52
- wall_material = Ra::Material.new(base: Ra::Pattern::Stripes.new(
53
- colors: [
54
- Ra::Color.hex('#94a3b8'),
55
- Ra::Color.hex('#475569'),
56
- ],
57
- transform: Ra::Transform
58
- .rotate_x(Math::PI / 4)
59
- .rotate_y(Math::PI / 4)
60
- .scale(0.2, 0.2, 0.2),
61
- ))
62
-
63
49
  floor = Ra::Shape::Plane.new(
64
- material: floor_material,
50
+ material: Ra::Material.new(base: Ra::Pattern::Checkers.new(
51
+ colors: [
52
+ Ra::Color.hex('#e2e8f0'),
53
+ Ra::Color.hex('#94a3b8'),
54
+ ],
55
+ )),
65
56
  )
66
57
 
67
58
  wall_l = Ra::Shape::Plane.new(
68
- material: wall_material,
59
+ material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
60
+ colors: [
61
+ Ra::Color.hex('#94a3b8'),
62
+ Ra::Color.hex('#475569'),
63
+ ],
64
+ )),
69
65
  transform: Ra::Transform
70
66
  .translate(0, 0, +5.0)
71
67
  .rotate_y(-Math::PI / 4)
@@ -73,7 +69,12 @@ wall_l = Ra::Shape::Plane.new(
73
69
  )
74
70
 
75
71
  wall_r = Ra::Shape::Plane.new(
76
- material: wall_material,
72
+ material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
73
+ colors: [
74
+ Ra::Color.hex('#94a3b8'),
75
+ Ra::Color.hex('#475569'),
76
+ ],
77
+ )),
77
78
  transform: Ra::Transform
78
79
  .translate(0, 0, +5.0)
79
80
  .rotate_y(Math::PI / 4)
@@ -81,18 +82,10 @@ wall_r = Ra::Shape::Plane.new(
81
82
  )
82
83
 
83
84
  sphere = Ra::Shape::Sphere.new(
84
- material: Ra::Material.new(base: Ra::Pattern::Rings.new(
85
- colors: [
86
- Ra::Color.hex('#f87171'),
87
- Ra::Color.hex('#dc2626'),
88
- ],
89
- transform: Ra::Transform
90
- .rotate_x(Math::PI / 4)
91
- .rotate_y(Math::PI / 4)
92
- .scale(0.2, 0.2, 0.2),
93
- )),
85
+ material: Ra::Material.new(base: earth),
94
86
  transform: Ra::Transform
95
87
  .translate(0, +0.5, -2.0)
88
+ .rotate_y(Math::PI / 2)
96
89
  .scale(0.5, 0.5, 0.5),
97
90
  )
98
91
 
@@ -100,9 +93,6 @@ cube_l = Ra::Shape::Cube.new(
100
93
  material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
101
94
  color_a: Ra::Color.hex('#f43f5e'),
102
95
  color_b: Ra::Color.hex('#8b5cf6'),
103
- transform: Ra::Transform
104
- .translate(1.0, 1.0, 1.0)
105
- .scale(3.0, 3.0, 3.0),
106
96
  )),
107
97
  transform: Ra::Transform
108
98
  .translate(+1.0, +0.3, -1.5)
@@ -113,15 +103,17 @@ cube_r = Ra::Shape::Cube.new(
113
103
  material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
114
104
  color_a: Ra::Color.hex('#84cc16'),
115
105
  color_b: Ra::Color.hex('#f97316'),
116
- transform: Ra::Transform
117
- .translate(1.0, 1.0, 1.0)
118
- .scale(3.0, 3.0, 3.0),
119
106
  )),
120
107
  transform: Ra::Transform
121
- .translate(-1.0, -0.3, -1.5)
108
+ .translate(-1.0, +0.3, -1.5)
122
109
  .scale(0.3, 0.3, 0.3),
123
110
  )
124
111
 
112
+ lights = [
113
+ light_l,
114
+ light_r,
115
+ ]
116
+
125
117
  shapes = [
126
118
  floor,
127
119
  wall_l,
@@ -131,7 +123,7 @@ shapes = [
131
123
  cube_r,
132
124
  ].freeze
133
125
 
134
- world = Ra::World.new(light:, shapes:)
126
+ world = Ra::World.new(lights:, shapes:)
135
127
  engine = Ra::Engine.new(camera:, world:)
136
128
  canvas = engine.render
137
129
 
data/lib/ra/camera.rb CHANGED
@@ -82,7 +82,7 @@ module Ra
82
82
  half_h - offset_y
83
83
  end
84
84
 
85
- # @param y [Float]
85
+ # @param x [Float]
86
86
  # @return [Float]
87
87
  def world_x(x:)
88
88
  offset_x = (x + 0.5) * p_size
data/lib/ra/canvas.rb CHANGED
@@ -16,19 +16,12 @@ module Ra
16
16
  class Canvas
17
17
  attr_accessor :w, :h, :precision
18
18
 
19
- DEFAULT_COLOR = Color.black
20
- private_constant :DEFAULT_COLOR
21
-
22
- DEFAULT_PRECISION = 255
23
- private_constant :DEFAULT_PRECISION
24
-
25
19
  PPM_VERSION = 'P3'
26
- private_constant :PPM_VERSION
27
20
 
28
21
  # @param w [Integer]
29
22
  # @param h [Integer]
30
23
  # @param precision [Integer]
31
- def initialize(w:, h:, precision: DEFAULT_PRECISION)
24
+ def initialize(w:, h:, precision: Color::PRECISION)
32
25
  @w = w
33
26
  @h = h
34
27
  @precision = precision
@@ -43,7 +36,7 @@ module Ra
43
36
  raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
44
37
  raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
45
38
 
46
- @pixels[x][y] || DEFAULT_COLOR
39
+ @pixels[x][y] || Color.black
47
40
  end
48
41
 
49
42
  # @param x [Integer]
data/lib/ra/color.rb CHANGED
@@ -19,8 +19,17 @@ module Ra
19
19
  class Color
20
20
  attr_accessor :r, :g, :b
21
21
 
22
- DEFAULT_PRECISION = 255
23
- private_constant :DEFAULT_PRECISION
22
+ PRECISION = 255
23
+
24
+ # @param value [Array<Numeric,Numeric,Numeric>]
25
+ # @return [Ra::Color]
26
+ def self.[](value)
27
+ new(
28
+ r: value[0],
29
+ g: value[1],
30
+ b: value[2],
31
+ )
32
+ end
24
33
 
25
34
  # @param value [String] e.g. "#336699"
26
35
  # @return [Ra::Color]
@@ -28,9 +37,9 @@ module Ra
28
37
  r, g, b = value.match(/^#(..)(..)(..)$/).captures.map(&:hex)
29
38
 
30
39
  new(
31
- r: Float(r) / DEFAULT_PRECISION,
32
- g: Float(g) / DEFAULT_PRECISION,
33
- b: Float(b) / DEFAULT_PRECISION,
40
+ r: Float(r) / PRECISION,
41
+ g: Float(g) / PRECISION,
42
+ b: Float(b) / PRECISION,
34
43
  )
35
44
  end
36
45
 
@@ -61,7 +70,7 @@ module Ra
61
70
 
62
71
  # @param precision [Integer]
63
72
  # @return [Integer]
64
- def ppm(precision: DEFAULT_PRECISION)
73
+ def ppm(precision: PRECISION)
65
74
  "#{r_val(precision:)} #{g_val(precision:)} #{b_val(precision:)}"
66
75
  end
67
76
 
@@ -123,17 +132,17 @@ module Ra
123
132
  protected
124
133
 
125
134
  # @return [Integer]
126
- def r_val(precision: DEFAULT_PRECISION)
135
+ def r_val(precision: PRECISION)
127
136
  val(value: r, precision:)
128
137
  end
129
138
 
130
139
  # @return [Integer]
131
- def g_val(precision: DEFAULT_PRECISION)
140
+ def g_val(precision: PRECISION)
132
141
  val(value: g, precision:)
133
142
  end
134
143
 
135
144
  # @return [Integer]
136
- def b_val(precision: DEFAULT_PRECISION)
145
+ def b_val(precision: PRECISION)
137
146
  val(value: b, precision:)
138
147
  end
139
148
 
@@ -142,7 +151,7 @@ module Ra
142
151
  # @param value [Numeric]
143
152
  # @param precision [Integer]
144
153
  # @return [Integer]
145
- def val(value:, precision: DEFAULT_PRECISION)
154
+ def val(value:, precision: PRECISION)
146
155
  (value * precision).clamp(0, precision).round
147
156
  end
148
157
  end
data/lib/ra/lighting.rb CHANGED
@@ -14,7 +14,6 @@ module Ra
14
14
  @light = light
15
15
  end
16
16
 
17
- # @param shadowed [Boolean]
18
17
  # @return [Ra::Color]
19
18
  def color
20
19
  ambient_color + diffuse_color + specular_color
@@ -22,7 +21,7 @@ module Ra
22
21
 
23
22
  private
24
23
 
25
- # @param [Ra::Shape]
24
+ # @return [Ra::Shape]
26
25
  def shape
27
26
  surface.shape
28
27
  end
data/lib/ra/material.rb CHANGED
@@ -18,7 +18,7 @@ module Ra
18
18
  # @param diffuse [Float] between 0.0 and 1.0
19
19
  # @param specular [Float] between 0.0 and 1.0
20
20
  # @param shininess [Numeric]
21
- def initialize(base:, ambient: 0.2, diffuse: 0.6, specular: 0.6, shininess: 200)
21
+ def initialize(base:, ambient: 0.0, diffuse: 0.8, specular: 0.2, shininess: 80)
22
22
  @base = base
23
23
  @ambient = ambient
24
24
  @diffuse = diffuse
@@ -2,29 +2,12 @@
2
2
 
3
3
  module Ra
4
4
  module Pattern
5
- # An abstract pattern. Any concrete subclass of pattern must implement the
6
- # method `local_color`.
5
+ # An abstract pattern. Any concrete subclass of pattern must implement the method `color`.
7
6
  class Base
8
- attr_accessor :transform
9
-
10
- # @param transform [Ra::Matrix]
11
- def initialize(transform: Transform::IDENTITY)
12
- @transform = transform
13
- end
14
-
15
- # @param point [Vector]
7
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
16
8
  # @return [Ra::Color]
17
9
  def color(point:)
18
- local_point = transform.inverse * point
19
- local_color(local_point:)
20
- end
21
-
22
- protected
23
-
24
- # @param local_point [Vector]
25
- # @return [Ra::Color]
26
- def local_color(local_point:)
27
- raise NotImplementedError, '#local_color must be implemented by a concrete subclass'
10
+ raise NotImplementedError, '#color must be implemented by a concrete subclass'
28
11
  end
29
12
  end
30
13
  end
@@ -4,28 +4,33 @@ module Ra
4
4
  module Pattern
5
5
  # A checkers pattern that alternates colors using:
6
6
  #
7
- # colors[⌊√(point.x² + point.z²)⌋]
7
+ # colors[⌊u * rows⌋ + ⌊v * cols)⌋ % colors.count]
8
8
  class Checkers < Base
9
- attr_accessor :colors
9
+ DEFAULT_ROWS = 2
10
+ DEFAULT_COLS = 2
11
+ DEFAULT_COLORS = [
12
+ Color.black,
13
+ Color.white,
14
+ ].freeze
10
15
 
16
+ # @param rows [Integer]
17
+ # @param cols [Integer]
11
18
  # @param colors [Array<Ra::Color>]
12
- # @param transform [Ra::Matrix]
13
- def initialize(colors:, transform: Transform::IDENTITY)
14
- super(transform:)
19
+ def initialize(cols: DEFAULT_COLS, rows: DEFAULT_ROWS, colors: DEFAULT_COLORS)
20
+ super()
21
+ @rows = rows
22
+ @cols = cols
15
23
  @colors = colors
16
24
  end
17
25
 
18
- protected
19
-
20
- # @param local_point [Vector]
26
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
21
27
  # @return [Ra::Color]
22
- def local_color(local_point:)
23
- x = local_point[0]
24
- y = local_point[1]
25
- z = local_point[2]
26
- index = x.floor + y.floor + z.floor
28
+ def color(point:)
29
+ u = point[0]
30
+ v = point[1]
31
+ index = (u * @rows).floor + (v * @cols).floor
27
32
 
28
- colors[index % colors.count]
33
+ @colors[index % @colors.count]
29
34
  end
30
35
  end
31
36
  end
@@ -4,25 +4,24 @@ module Ra
4
4
  module Pattern
5
5
  # A graident pattern from `color_a` to `color_b` using:
6
6
  #
7
- # color_b + (color_b - color_a) * (point.x - point.x.floor)
7
+ # color_b + (color_b - color_a) * (u + v) / 2
8
8
  class Gradient < Base
9
9
  # @param color_a [Ra::Color]
10
10
  # @param color_b [Ra::Color]
11
- # @param transform [Ra::Matrix]
12
- def initialize(color_a:, color_b:, transform: Transform::IDENTITY)
13
- super(transform:)
11
+ def initialize(color_a:, color_b:)
12
+ super()
14
13
  @color_a = color_a
15
14
  @color_b = color_b
16
15
  end
17
16
 
18
- protected
19
-
20
- # @param local_point [Vector]
17
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
21
18
  # @return [Ra::Color]
22
- def local_color(local_point:)
23
- value = local_point[0]
24
- fraction = value - value.floor
25
- @color_a + ((@color_b - @color_a) * fraction)
19
+ def color(point:)
20
+ u = point[0]
21
+ v = point[1]
22
+ value = (u + v) / 2
23
+
24
+ @color_a + ((@color_b - @color_a) * value)
26
25
  end
27
26
  end
28
27
  end
@@ -4,27 +4,22 @@ module Ra
4
4
  module Pattern
5
5
  # A rings pattern that alternates colors using:
6
6
  #
7
- # colors[⌊√(point.x² + point.z²)⌋]
7
+ # colors[⌊√(u² + v²)⌋]
8
8
  class Rings < Base
9
- attr_accessor :colors
10
-
11
- # @param colors [Array<Rays::Color>]
12
- # @param transform [Rays::Matrix]
13
- def initialize(colors:, transform: DEFAULT_TRANSFORM)
14
- super(transform:)
9
+ # @param colors [Array<Ra::Color>]
10
+ def initialize(colors:)
11
+ super()
15
12
  @colors = colors
16
13
  end
17
14
 
18
- protected
19
-
20
- # @param local_point [Vector]
21
- # @return [Rays::Color]
22
- def local_color(local_point:)
23
- x = local_point[0]
24
- z = local_point[2]
25
- index = Math.sqrt((x**2) + (z**2)).floor
15
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
16
+ # @return [Ra::Color]
17
+ def color(point:)
18
+ u = point[0]
19
+ v = point[1]
20
+ index = Math.sqrt((u**2) + (v**2)).floor
26
21
 
27
- colors[index % colors.count]
22
+ @colors[index % @colors.count]
28
23
  end
29
24
  end
30
25
  end
@@ -6,24 +6,21 @@ module Ra
6
6
  #
7
7
  # colors[⌊point.x⌋]
8
8
  class Stripes < Base
9
- attr_accessor :colors
10
-
11
9
  # @param colors [Array<Ra::Color>]
12
- # @param transform [Ra::Matrix]
13
- def initialize(colors:, transform: Transform::IDENTITY)
14
- super(transform:)
10
+ def initialize(colors:)
11
+ super()
15
12
  @colors = colors
16
13
  end
17
14
 
18
- protected
19
-
20
- # @param local_point [Vector]
15
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
21
16
  # @return [Ra::Color]
22
- def local_color(local_point:)
23
- x = local_point[0]
24
- index = x.floor
17
+ def color(point:)
18
+ count = @colors.count
19
+ u = point[0]
20
+ v = point[1]
21
+ value = (u + v) * (2 * count)
25
22
 
26
- colors[index % colors.count]
23
+ @colors[value.floor % count]
27
24
  end
28
25
  end
29
26
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mini_magick'
4
+
5
+ module Ra
6
+ module Pattern
7
+ # A texture that can load an AVIF / JPG / PNG / BMP:
8
+ #
9
+ # colors[⌊point.x⌋]
10
+ class Texture < Base
11
+ # @param path [Pathname]
12
+ def initialize(path:)
13
+ super()
14
+ @path = path
15
+ end
16
+
17
+ # @param point [Array<Numeric,Numeric>] <u = 0.0..1.0, v = 0.0..1.0>
18
+ # @return [Ra::Color]
19
+ def color(point:)
20
+ pixel = pixel(point:)
21
+
22
+ Color.new(
23
+ r: Float(pixel[0]) / Color::PRECISION,
24
+ g: Float(pixel[1]) / Color::PRECISION,
25
+ b: Float(pixel[2]) / Color::PRECISION,
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ # @return [Array<0..255,0..255,0..255>]
32
+ def pixel(point:)
33
+ u = point[0]
34
+ v = 1.0 - point[1]
35
+
36
+ x = (u * image.width.pred).round
37
+ y = (v * image.height.pred).round
38
+
39
+ pixels[y][x]
40
+ end
41
+
42
+ # @return [Array<Array<0..255,0..255,0.255>>]
43
+ def pixels
44
+ @pixels ||= image.get_pixels
45
+ end
46
+
47
+ def image
48
+ @image ||= MiniMagick::Image.open(@path)
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/ra/shape/base.rb CHANGED
@@ -6,7 +6,7 @@ module Ra
6
6
  # methods `l_normal` and `t_intersect`. Both methods use a point / ray
7
7
  # with a local transform applied.
8
8
  class Base
9
- attr_accessor :material, :transform
9
+ attr_accessor :material
10
10
 
11
11
  # @param material [Ra::Material]
12
12
  # @param transform [Ra::Matrix]
@@ -18,25 +18,29 @@ module Ra
18
18
  # @param ray [Ra::Ray]
19
19
  # @return [Array<Ra::Intersection>]
20
20
  def intersect(ray:)
21
- t_intersect(ray: ray.transform(transform.inverse))
21
+ t_intersect(ray: ray.transform(@transform.inverse))
22
22
  .map { |t| Ra::Intersection.new(ray:, shape: self, t:) }
23
23
  end
24
24
 
25
- # @param point [Vector]
26
- # @return [Vector]
25
+ # @param point [Vector] <x, y, z, Tuple::POINT>
26
+ # @return [Vector] <x, y, z, Tuple::POINT>
27
27
  def normal(point:)
28
- normal = transform.inverse.transpose * l_normal(point: transform.inverse * point)
28
+ normal = @transform.inverse.transpose * l_normal(point: @transform.inverse * point)
29
29
 
30
30
  Vector[normal[0], normal[1], normal[2], Ra::Tuple::VECTOR].normalize
31
31
  end
32
32
 
33
- # @param point [Vector]
33
+ # @param point [Vector] <x, y, z, Tuple::POINT>
34
34
  # @return [Color]
35
35
  def color(point:)
36
- @material.color(point: transform.inverse * point)
36
+ @material.color(point: uv_point(point: @transform.inverse * point))
37
37
  end
38
38
 
39
- private
39
+ # @param point [Vector] <x, y, z, Tuple::POINT>
40
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
41
+ def uv_point(point:)
42
+ raise NotImplementedError, '#uv_point must be implemented by a concrete subclass'
43
+ end
40
44
 
41
45
  # @param ray [Ra::Ray] local
42
46
  # @return [Array<Intersection>]
data/lib/ra/shape/cube.rb CHANGED
@@ -21,7 +21,20 @@ module Ra
21
21
  #
22
22
  # Thus 6 planes can be checked for intersect.
23
23
  class Cube < Base
24
- protected
24
+ # @param point [Vector] <x, y, z, Tuple::POINT>
25
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
26
+ def uv_point(point:)
27
+ x = point[0]
28
+ y = point[1]
29
+ z = point[2]
30
+ value = [x, y, z].max_by(&:abs)
31
+
32
+ case value
33
+ when x then x.positive? ? uv_point_r(point:) : uv_point_l(point:)
34
+ when y then y.positive? ? uv_point_u(point:) : uv_point_d(point:)
35
+ else z.positive? ? uv_point_f(point:) : uv_point_b(point:)
36
+ end
37
+ end
25
38
 
26
39
  # @param ray [Ra::Ray] local
27
40
  # @return [Array<Numeric>]
@@ -68,7 +81,6 @@ module Ra
68
81
 
69
82
  # @param origin [Numeric]
70
83
  # @param direction [Numeric]
71
- # @param value [Numeric]
72
84
  # @return [Array<Numeric,Numeric>]
73
85
  def t_min_max(origin, direction)
74
86
  t_min_numerator = -1 - origin
@@ -84,6 +96,60 @@ module Ra
84
96
 
85
97
  t_min < t_max ? [t_min, t_max] : [t_max, t_min]
86
98
  end
99
+
100
+ # @param point [Vector] <x, y, z, Tuple::POINT>
101
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
102
+ def uv_point_u(point:)
103
+ Vector[
104
+ ((point[0] + 1) % 2) / 2,
105
+ ((1 - point[2]) % 2) / 2,
106
+ ]
107
+ end
108
+
109
+ # @param point [Vector] <x, y, z, Tuple::POINT>
110
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
111
+ def uv_point_d(point:)
112
+ Vector[
113
+ ((point[0] + 1) % 2) / 2,
114
+ ((point[2] + 1) % 2) / 2,
115
+ ]
116
+ end
117
+
118
+ # @param point [Vector] <x, y, z, Tuple::POINT>
119
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
120
+ def uv_point_l(point:)
121
+ Vector[
122
+ ((point[2] + 1) % 2) / 2,
123
+ ((point[1] + 1) % 2) / 2,
124
+ ]
125
+ end
126
+
127
+ # @param point [Vector] <x, y, z, Tuple::POINT>
128
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
129
+ def uv_point_r(point:)
130
+ Vector[
131
+ ((1 - point[2]) % 2) / 2,
132
+ ((point[1] + 1) % 2) / 2,
133
+ ]
134
+ end
135
+
136
+ # @param point [Vector] <x, y, z, Tuple::POINT>
137
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
138
+ def uv_point_b(point:)
139
+ Vector[
140
+ ((1 - point[0]) % 2) / 2,
141
+ ((point[1] + 1) % 2) / 2,
142
+ ]
143
+ end
144
+
145
+ # @param point [Vector] <x, y, z, Tuple::POINT>
146
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
147
+ def uv_point_f(point:)
148
+ Vector[
149
+ ((point[0] + 1) % 2) / 2,
150
+ ((point[1] + 1) % 2) / 2,
151
+ ]
152
+ end
87
153
  end
88
154
  end
89
155
  end
@@ -18,7 +18,14 @@ module Ra
18
18
  #
19
19
  # A direction.y < EPISLON indicates the ray does not intersect the plane.
20
20
  class Plane < Base
21
- protected
21
+ # @param point [Vector] <x, y, z, Tuple::POINT>
22
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
23
+ def uv_point(point:)
24
+ Vector[
25
+ point[0] % 1, # u = x % 1
26
+ point[2] % 1, # v = y % 2
27
+ ]
28
+ end
22
29
 
23
30
  # @param ray [Ra::Ray] local
24
31
  # @return [Array<Numeric>]
@@ -32,7 +32,22 @@ module Ra
32
32
  #
33
33
  # A discriminant <0 indicates the ray does not intersect the sphere.
34
34
  class Sphere < Base
35
- protected
35
+ # @param point [Vector] <x, y, z, Tuple::POINT>
36
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
37
+ def uv_point(point:)
38
+ x = point[0]
39
+ y = point[1]
40
+ z = point[2]
41
+
42
+ radius = Vector[x, y, z].magnitude
43
+ theta = Math.atan2(x, z)
44
+ phi = Math.acos(y / radius)
45
+
46
+ u = 1 - ((theta / (2 * Math::PI)) + 0.5)
47
+ v = 1 - (phi / Math::PI)
48
+
49
+ Vector[u, v]
50
+ end
36
51
 
37
52
  # @param ray [Ra::Ray] local
38
53
  # @return [Array<Numeric>]
data/lib/ra/surface.rb CHANGED
@@ -18,7 +18,7 @@ module Ra
18
18
 
19
19
  # @return [Vector]
20
20
  def hpoint
21
- point + (normalv * EPSILON)
21
+ @hpoint ||= point + (normalv * EPSILON)
22
22
  end
23
23
  end
24
24
  end
data/lib/ra/tuple.rb CHANGED
@@ -9,8 +9,8 @@ module Ra
9
9
  POINT = 1
10
10
  VECTOR = 0
11
11
 
12
- # @return source [Vector]
13
- # @return target [Vector]
12
+ # @param source [Vector]
13
+ # @param target [Vector]
14
14
  # @return [Vector]
15
15
  def self.cross(source, target)
16
16
  cross = Vector[source[0], source[1], source[2]].cross(Vector[target[0], target[1], target[2]])
data/lib/ra/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ra
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/ra/world.rb CHANGED
@@ -3,12 +3,10 @@
3
3
  module Ra
4
4
  # A world is composed of objects (lights / cameras) and handles coloring of rays.
5
5
  class World
6
- attr_accessor :light, :shapes
7
-
8
- # @param light [Ra::Light]
6
+ # @param lights [Ra::Light]
9
7
  # @param shapes [Array<Ra::Shape>]
10
- def initialize(light:, shapes:)
11
- @light = light
8
+ def initialize(lights:, shapes:)
9
+ @lights = lights
12
10
  @shapes = shapes
13
11
  end
14
12
 
@@ -30,14 +28,20 @@ module Ra
30
28
  # @return [Ra::Color]
31
29
  def color(intersection:)
32
30
  surface = intersection.surface
33
- shadowed = shadowed?(point: surface.hpoint)
31
+ point = surface.hpoint
32
+
33
+ colors = @lights.map do |light|
34
+ shadowed = shadowed?(point:, light:)
35
+ lighting = Lighting.new(surface:, light:, shadowed:)
36
+ lighting.color
37
+ end
34
38
 
35
- Lighting.new(shadowed:, surface:, light:).color
39
+ colors.reduce(&:+)
36
40
  end
37
41
 
38
42
  # @param point [Ra::Point]
39
- def shadowed?(point:)
40
- vector = @light.position - point
43
+ def shadowed?(light:, point:)
44
+ vector = light.position - point
41
45
  distance = vector.magnitude
42
46
  direction = vector.normalize
43
47
  ray = Ray.new(origin: point, direction:)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-22 00:00:00.000000000 Z
11
+ date: 2023-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: matrix
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mini_magick
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: slop
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -77,6 +91,7 @@ files:
77
91
  - lib/ra/pattern/gradient.rb
78
92
  - lib/ra/pattern/rings.rb
79
93
  - lib/ra/pattern/stripes.rb
94
+ - lib/ra/pattern/texture.rb
80
95
  - lib/ra/ray.rb
81
96
  - lib/ra/shape/base.rb
82
97
  - lib/ra/shape/cube.rb