ra 0.2.0 → 0.4.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: 96608d9848ea1181d69813cd6a37cfcc5cb0ac04564048ed4b2a9d6f4af27414
4
+ data.tar.gz: a1bfbd9254cc6c56abab623bdf65d9a3282904c4b43039c3609934a2d8d273b1
5
5
  SHA512:
6
- metadata.gz: 862fb845e4379218540100334fb7366f9ecf1674d1026da473ddc6c783bf228b6483512bf51d0c83218ec650ff1959e61a48756953da1a6118aae165c4057f77
7
- data.tar.gz: 9d21e31c6fda9b0795316ea2a9690fd087d646b5758ab873d82fb2aebec377cd08327a0eefde9cea00d8ff4e1088ddbb015691f7e16baee509b967918b07bcd6
6
+ metadata.gz: b63eba61eb50a16fc55388b409b27dde40e092009f920ffe614fb15e29683ac58b129a2bf3848c0944b220ed2b6aca1cfb434a2441d050a601a801fb60c5e0ae
7
+ data.tar.gz: 004eb7ca2f72a3804f62332f1f909cd2875dbc46928ea6392e22b32a34ae5a3ea55117a6bda5dfd15b84b33d8f1c3d990517b10b1d64ed08497e3fe5b8619750
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
@@ -8,8 +8,8 @@ require 'slop'
8
8
  config = Slop.parse(ARGV) do |options|
9
9
  options.banner = 'Usage: ra -w 2560 -h 2048 | convert - sample.avif'
10
10
 
11
- options.integer '-w', '--width', 'width', default: 1280
12
- options.integer '-h', '--height', 'height', default: 1024
11
+ options.integer '-w', '--width', 'width', default: 2560
12
+ options.integer '-h', '--height', 'height', default: 2048
13
13
  options.integer '-fov', 'degrees', default: 60
14
14
 
15
15
  options.on('--help', 'help') do
@@ -23,9 +23,16 @@ config = Slop.parse(ARGV) do |options|
23
23
  end
24
24
  end
25
25
 
26
- light = Ra::Light.new(
27
- position: Vector[+5, +7, -9, Ra::Tuple::POINT],
28
- intensity: Ra::Color.white,
26
+ earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'textures/earth.avif'))
27
+
28
+ light_l = Ra::Light.new(
29
+ position: Vector[+5, +3, -9, Ra::Tuple::POINT],
30
+ intensity: Ra::Color.uniform(0.5),
31
+ )
32
+
33
+ light_r = Ra::Light.new(
34
+ position: Vector[-5, +3, -9, Ra::Tuple::POINT],
35
+ intensity: Ra::Color.uniform(0.5),
29
36
  )
30
37
 
31
38
  camera = Ra::Camera.new(
@@ -33,95 +40,98 @@ camera = Ra::Camera.new(
33
40
  h: config[:h],
34
41
  fov: config[:fov] * Math::PI / 180,
35
42
  transform: Ra::Transform.view(
36
- from: Vector[0, +1.5, -5.0, Ra::Tuple::POINT],
37
- to: Vector[0, 0, +1.0, Ra::Tuple::POINT],
43
+ from: Vector[0, +1.5, -4.0, Ra::Tuple::POINT],
44
+ to: Vector[0, 0, 0, Ra::Tuple::POINT],
38
45
  up: Vector[0, 1, 0, Ra::Tuple::VECTOR],
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
- ],
49
+ floor = Ra::Shape::Plane.new(
50
+ material: Ra::Material.new(
51
+ base: Ra::Pattern::Checkers.new(
52
+ colors: [
53
+ Ra::Color.hex('#e2e8f0'),
54
+ Ra::Color.hex('#1e293b'),
55
+ ],
56
+ ),
57
+ reflective: 0.2,
58
+ ),
57
59
  transform: Ra::Transform
58
- .rotate_x(Math::PI / 4)
59
60
  .rotate_y(Math::PI / 4)
60
- .scale(0.2, 0.2, 0.2),
61
- ))
62
-
63
- floor = Ra::Shape::Plane.new(
64
- material: floor_material,
61
+ .scale(0.5, 0.5, 0.5),
65
62
  )
66
63
 
67
64
  wall_l = Ra::Shape::Plane.new(
68
- material: wall_material,
65
+ material: Ra::Material.new(
66
+ base: Ra::Pattern::Stripes.new(
67
+ colors: [
68
+ Ra::Color.hex('#f5f5f4'),
69
+ Ra::Color.hex('#e7e5e4'),
70
+ ],
71
+ ),
72
+ ),
69
73
  transform: Ra::Transform
70
- .translate(0, 0, +5.0)
74
+ .translate(0, 0, +3.0)
71
75
  .rotate_y(-Math::PI / 4)
72
76
  .rotate_x(Math::PI / 2),
73
77
  )
74
78
 
75
79
  wall_r = Ra::Shape::Plane.new(
76
- material: wall_material,
80
+ material: Ra::Material.new(
81
+ base: Ra::Pattern::Stripes.new(
82
+ colors: [
83
+ Ra::Color.hex('#f5f5f4'),
84
+ Ra::Color.hex('#e7e5e4'),
85
+ ],
86
+ ),
87
+ ),
77
88
  transform: Ra::Transform
78
- .translate(0, 0, +5.0)
89
+ .translate(0, 0, +3.0)
79
90
  .rotate_y(Math::PI / 4)
80
91
  .rotate_x(Math::PI / 2),
81
92
  )
82
93
 
83
94
  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
- )),
95
+ material: Ra::Material.new(base: earth),
94
96
  transform: Ra::Transform
95
97
  .translate(0, +0.5, -2.0)
98
+ .rotate_y(Math::PI / 2)
96
99
  .scale(0.5, 0.5, 0.5),
97
100
  )
98
101
 
99
102
  cube_l = Ra::Shape::Cube.new(
100
- material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
101
- color_a: Ra::Color.hex('#f43f5e'),
102
- 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
- )),
103
+ material: Ra::Material.new(
104
+ base: Ra::Pattern::Gradient.new(
105
+ color_a: Ra::Color.hex('#f43f5e'),
106
+ color_b: Ra::Color.hex('#8b5cf6'),
107
+ ),
108
+ reflective: 0.2,
109
+ ),
107
110
  transform: Ra::Transform
108
- .translate(+1.0, +0.3, -1.5)
109
- .scale(0.3, 0.3, 0.3),
111
+ .translate(+1.0, +0.3, -1.0)
112
+ .scale(0.3, 0.3, 0.3)
113
+ .rotate_y(-Math::PI / 8),
110
114
  )
111
115
 
112
116
  cube_r = Ra::Shape::Cube.new(
113
- material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
114
- color_a: Ra::Color.hex('#84cc16'),
115
- 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
- )),
117
+ material: Ra::Material.new(
118
+ base: Ra::Pattern::Gradient.new(
119
+ color_a: Ra::Color.hex('#84cc16'),
120
+ color_b: Ra::Color.hex('#f97316'),
121
+ ),
122
+ reflective: 0.5,
123
+ ),
120
124
  transform: Ra::Transform
121
- .translate(-1.0, -0.3, -1.5)
122
- .scale(0.3, 0.3, 0.3),
125
+ .translate(-1.0, +0.3, -1.0)
126
+ .scale(0.3, 0.3, 0.3)
127
+ .rotate_y(+Math::PI / 8),
123
128
  )
124
129
 
130
+ lights = [
131
+ light_l,
132
+ light_r,
133
+ ]
134
+
125
135
  shapes = [
126
136
  floor,
127
137
  wall_l,
@@ -131,7 +141,7 @@ shapes = [
131
141
  cube_r,
132
142
  ].freeze
133
143
 
134
- world = Ra::World.new(light:, shapes:)
144
+ world = Ra::World.new(lights:, shapes:)
135
145
  engine = Ra::Engine.new(camera:, world:)
136
146
  canvas = engine.render
137
147
 
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/engine.rb CHANGED
@@ -31,10 +31,7 @@ module Ra
31
31
  def draw(x:, y:, canvas:)
32
32
  ray = @camera.ray(x:, y:)
33
33
 
34
- intersections = @world.intersect(ray:)
35
- intersection = Intersection.hit(intersections:)
36
-
37
- canvas[x, y] = @world.color(intersection:) if intersection
34
+ canvas[x, y] = @world.color(ray:)
38
35
  end
39
36
  end
40
37
  end
@@ -38,8 +38,9 @@ module Ra
38
38
  point = ray.position(t:)
39
39
  eyev = -ray.direction
40
40
  normalv = shape.normal(point:)
41
+ reflectv = Tuple.reflect(ray.direction, normalv)
41
42
 
42
- Surface.new(shape:, eyev:, normalv:, point:)
43
+ Surface.new(shape:, eyev:, normalv:, reflectv:, point:)
43
44
  end
44
45
  end
45
46
  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
@@ -54,7 +53,7 @@ module Ra
54
53
 
55
54
  # @return [Ra::Vector]
56
55
  def reflectv
57
- @reflectv ||= -(lightv - (normalv * 2 * lightv.dot(normalv)))
56
+ @reflectv ||= -Tuple.reflect(lightv, normalv)
58
57
  end
59
58
 
60
59
  # @return [Ra::Vector]
data/lib/ra/material.rb CHANGED
@@ -11,17 +11,24 @@ module Ra
11
11
  # shininess: 200,
12
12
  # )
13
13
  class Material
14
- attr_accessor :base, :ambient, :diffuse, :specular, :shininess
14
+ attr_accessor :base, :ambient, :diffuse, :reflective, :specular, :shininess
15
15
 
16
16
  # @param base [Ra::Color, Ra::Pattern:::Base]
17
17
  # @param ambient [Float] between 0.0 and 1.0
18
18
  # @param diffuse [Float] between 0.0 and 1.0
19
+ # @param reflective [Float] between 0.0 and 1.0
19
20
  # @param specular [Float] between 0.0 and 1.0
20
21
  # @param shininess [Numeric]
21
- def initialize(base:, ambient: 0.2, diffuse: 0.6, specular: 0.6, shininess: 200)
22
+ def initialize(base:, ambient: 0.0, diffuse: 0.8, reflective: 0.0, specular: 0.2, shininess: 80)
23
+ raise ArgumentError, "ambient=#{ambient} must be between 0 and 1" unless ambient.between?(0, 1)
24
+ raise ArgumentError, "ambient=#{diffuse} must be between 0 and 1" unless diffuse.between?(0, 1)
25
+ raise ArgumentError, "ambient=#{reflective} must be between 0 and 1" unless reflective.between?(0, 1)
26
+ raise ArgumentError, "specular=#{specular} must be between 0 and 1" unless specular.between?(0, 1)
27
+
22
28
  @base = base
23
29
  @ambient = ambient
24
30
  @diffuse = diffuse
31
+ @reflective = reflective
25
32
  @specular = specular
26
33
  @shininess = shininess
27
34
  end
@@ -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
@@ -3,22 +3,23 @@
3
3
  module Ra
4
4
  # A surface contains everything needed to apply lighting.
5
5
  class Surface
6
- attr_accessor :eyev, :normalv, :shape, :point
6
+ attr_accessor :eyev, :normalv, :reflectv, :shape, :point
7
7
 
8
8
  # @param eyev [Vector]
9
9
  # @param normalv [Vector]
10
10
  # @param shape [Ra::Shape]
11
11
  # @param point [Vector]
12
- def initialize(eyev:, normalv:, shape:, point:)
12
+ def initialize(eyev:, normalv:, reflectv:, shape:, point:)
13
13
  @eyev = eyev
14
14
  @normalv = normalv.dot(eyev).negative? ? -normalv : +normalv
15
+ @reflectv = reflectv
15
16
  @shape = shape
16
17
  @point = point
17
18
  end
18
19
 
19
20
  # @return [Vector]
20
21
  def hpoint
21
- point + (normalv * EPSILON)
22
+ @hpoint ||= point + (normalv * EPSILON)
22
23
  end
23
24
  end
24
25
  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]])
@@ -22,5 +22,12 @@ module Ra
22
22
  Tuple::VECTOR,
23
23
  ]
24
24
  end
25
+
26
+ # @param vector [Vector]
27
+ # @param normal [Vector]
28
+ # @return [Vector]
29
+ def self.reflect(vector, normal)
30
+ vector - (normal * 2 * vector.dot(normal))
31
+ end
25
32
  end
26
33
  end
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.4.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
 
@@ -26,18 +24,42 @@ module Ra
26
24
  Intersection.hit(intersections:)
27
25
  end
28
26
 
29
- # @param intersection [Ra::Intersection]
27
+ # @param ray [Ra::Ray]
30
28
  # @return [Ra::Color]
31
- def color(intersection:)
29
+ def color(ray:, remaining: 4)
30
+ intersection = intersection(ray:)
31
+ return unless intersection
32
+
32
33
  surface = intersection.surface
33
- shadowed = shadowed?(point: surface.hpoint)
34
34
 
35
- Lighting.new(shadowed:, surface:, light:).color
35
+ colors = @lights.map do |light|
36
+ shadowed = shadowed?(point: surface.hpoint, light:)
37
+ lighting = Lighting.new(light:, shadowed:, surface:)
38
+ lighting.color
39
+ end
40
+
41
+ colors.reduce(&:+) + reflect(surface:, remaining:)
42
+ end
43
+
44
+ # @param surface [Ra::Surface]
45
+ # @param remaining [Integer]
46
+ # @return [Ra::Color]
47
+ def reflect(surface:, remaining:)
48
+ return if remaining.zero?
49
+
50
+ material = surface.shape.material
51
+ return unless material.reflective.positive?
52
+
53
+ ray = Ray.new(origin: surface.hpoint, direction: surface.reflectv)
54
+
55
+ color = color(ray:, remaining: remaining.pred)
56
+ color * material.reflective if color
36
57
  end
37
58
 
59
+ # @param light [Ra::Light]
38
60
  # @param point [Ra::Point]
39
- def shadowed?(point:)
40
- vector = @light.position - point
61
+ def shadowed?(light:, point:)
62
+ vector = light.position - point
41
63
  distance = vector.magnitude
42
64
  direction = vector.normalize
43
65
  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.4.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