ra 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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