ra 0.2.0 → 0.4.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: 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