ra 0.7.0 → 1.0.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: 3603ce3fb7fc5149c4d723fc2c57d14b590f31aa6097a365ca02b554cce464f5
4
- data.tar.gz: 5bed4817b7bbfaf3a6994cfb2450d6957802a1921ec560178c2cf01583921824
3
+ metadata.gz: d7ee2d6f85d018ae7eeb70e9796973277176ddac2940dbe442355cfd8f55bd5d
4
+ data.tar.gz: 7ca5f9394c0d2e62ba5348c3047b482554ebe8437646cc1fa100eea0141fa67d
5
5
  SHA512:
6
- metadata.gz: e0899f44711e31595f7d1a414092876f4a3bb485e84c0f35431dc479b93829e4dcc01272e29b5098ad46f52523f7927bcb96fc02e7e27f2f3b0933e50af17499
7
- data.tar.gz: 6b4bb9c3120bb240c7fa4c993de673a4dfe351825cf072a34d0464caaff43002507f17ea3b7f2dc0166dfaf45cb1817b032f419fbdfae6448b0c4a68e0dd7d04
6
+ metadata.gz: c6c66f296b05b5fb771dfaab9a95b0523c5ec3563da731c6f0e177a9bc775d7309c59b3e67d2e56c4ea8d070a009e4d0b7b0d84d167bcaa2644a9886857ae3f4
7
+ data.tar.gz: c6c2465c7cc20c95aaaa3802dc0d5ad54bedfa6d8fa023bb24f9e6dfad4144b45c58ede27c998d87167ff755883afce0d7385beb0341610fa1ddadbe143a0c90
data/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Ra
2
2
 
3
+ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ksylvest/ra/blob/main/LICENSE)
4
+ [![RubyGems](https://img.shields.io/gem/v/ra)](https://rubygems.org/gems/ra)
5
+ [![GitHub](https://img.shields.io/badge/github-repo-blue.svg)](https://github.com/ksylvest/ra)
6
+ [![Yard](https://img.shields.io/badge/docs-site-blue.svg)](https://ra.ksylvest.com)
7
+ [![CircleCI](https://img.shields.io/circleci/build/github/ksylvest/ra)](https://circleci.com/gh/ksylvest/ra)
8
+ [![Code Climate Maintainability](https://img.shields.io/codeclimate/maintainability/ksylvest/ra)](https://codeclimate.com/github/ksylvest/ra)
9
+ [![Code Climate Coverage](https://img.shields.io/codeclimate/coverage/ksylvest/ra)](https://codeclimate.com/github/ksylvest/ra)
10
+
3
11
  Named for [Ra](https://en.wikipedia.org/wiki/Ra) - arguably the original ray tracer.
4
12
 
5
13
  ## Installation
@@ -12,7 +20,7 @@ gem install ra
12
20
 
13
21
  ```sh
14
22
  ra -w 2560 -h 2048 > sample.ppm
15
- convert -quality 80 sample.ppm sample.avif
23
+ convert -quality 80 sample.ppm sample.avif
16
24
  ```
17
25
 
18
- ![Sample](./sample.avif)
26
+ ![Sample](https://github.com/ksylvest/ra/raw/main/sample.avif)
data/exe/ra CHANGED
@@ -10,7 +10,7 @@ config = Slop.parse(ARGV) do |options|
10
10
 
11
11
  options.integer '-w', '--width', 'width', default: 2560
12
12
  options.integer '-h', '--height', 'height', default: 2048
13
- options.integer '-fov', 'degrees', default: 60
13
+ options.integer '-fov', 'degrees', default: 90
14
14
 
15
15
  options.on('--help', 'help') do
16
16
  Ra.logger.log(options)
@@ -26,13 +26,13 @@ end
26
26
  earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'assets/earth.avif'))
27
27
 
28
28
  light_l = Ra::Light.new(
29
- position: Vector[+5, +3, -9, Ra::Tuple::POINT],
30
- intensity: Ra::Color.uniform(0.5),
29
+ position: Vector[+3.0, +2.0, -5.0, Ra::Tuple::POINT],
30
+ intensity: Ra::Color.uniform(0.75),
31
31
  )
32
32
 
33
33
  light_r = Ra::Light.new(
34
- position: Vector[-5, +3, -9, Ra::Tuple::POINT],
35
- intensity: Ra::Color.uniform(0.5),
34
+ position: Vector[-3.0, +2.0, -5.0, Ra::Tuple::POINT],
35
+ intensity: Ra::Color.uniform(0.75),
36
36
  )
37
37
 
38
38
  camera = Ra::Camera.new(
@@ -41,7 +41,7 @@ camera = Ra::Camera.new(
41
41
  fov: config[:fov] * Math::PI / 180,
42
42
  transform: Ra::Transform.view(
43
43
  from: Vector[0, +1.5, -4.0, Ra::Tuple::POINT],
44
- to: Vector[0, 0, 0, Ra::Tuple::POINT],
44
+ to: Vector[0, +0.5, 0, Ra::Tuple::POINT],
45
45
  up: Vector[0, 1, 0, Ra::Tuple::VECTOR],
46
46
  ),
47
47
  )
@@ -57,19 +57,28 @@ floor = Ra::Shape::Plane.new(
57
57
  reflective: 0.2,
58
58
  ),
59
59
  transform: Ra::Transform
60
- .rotate_y(Math::PI / 4)
61
- .scale(0.5, 0.5, 0.5),
60
+ .rotate_y(Math::PI / 4),
62
61
  )
63
62
 
64
- wall_l = Ra::Shape::Plane.new(
63
+ ceiling = Ra::Shape::Plane.new(
65
64
  material: Ra::Material.new(
66
65
  base: Ra::Pattern::Stripes.new(
67
66
  colors: [
68
- Ra::Color.hex('#f5f5f4'),
67
+ Ra::Color.hex('#e4e4e7'),
69
68
  Ra::Color.hex('#e7e5e4'),
70
69
  ],
71
70
  ),
72
71
  ),
72
+ transform: Ra::Transform
73
+ .translate(0, +3.0, 0)
74
+ .scale(0.2, 0.2, 0.2)
75
+ .rotate_y(Math::PI / 4),
76
+ )
77
+
78
+ wall_l = Ra::Shape::Plane.new(
79
+ material: Ra::Material.new(
80
+ base: Ra::Color.hex('#6b21a8'),
81
+ ),
73
82
  transform: Ra::Transform
74
83
  .translate(0, 0, +3.0)
75
84
  .rotate_y(-Math::PI / 4)
@@ -78,12 +87,7 @@ wall_l = Ra::Shape::Plane.new(
78
87
 
79
88
  wall_r = Ra::Shape::Plane.new(
80
89
  material: Ra::Material.new(
81
- base: Ra::Pattern::Stripes.new(
82
- colors: [
83
- Ra::Color.hex('#f5f5f4'),
84
- Ra::Color.hex('#e7e5e4'),
85
- ],
86
- ),
90
+ base: Ra::Color.hex('#9f1239'),
87
91
  ),
88
92
  transform: Ra::Transform
89
93
  .translate(0, 0, +3.0)
@@ -92,38 +96,41 @@ wall_r = Ra::Shape::Plane.new(
92
96
  )
93
97
 
94
98
  sphere = Ra::Shape::Sphere.new(
95
- material: Ra::Material.new(base: earth),
99
+ material: Ra::Material.new(
100
+ base: earth,
101
+ reflective: 0.3,
102
+ ),
96
103
  transform: Ra::Transform
97
- .translate(0, +0.5, -2.0)
104
+ .translate(0, +0.6, -2.0)
98
105
  .rotate_y(Math::PI / 2)
99
- .scale(0.5, 0.5, 0.5),
106
+ .scale(0.6, 0.6, 0.6),
100
107
  )
101
108
 
102
- cube_l = Ra::Shape::Cube.new(
109
+ cube = Ra::Shape::Cube.new(
103
110
  material: Ra::Material.new(
104
111
  base: Ra::Pattern::Gradient.new(
105
112
  color_a: Ra::Color.hex('#f43f5e'),
106
113
  color_b: Ra::Color.hex('#8b5cf6'),
107
114
  ),
108
- reflective: 0.2,
115
+ reflective: 0.3,
109
116
  ),
110
117
  transform: Ra::Transform
111
- .translate(+1.0, +0.3, -1.0)
112
- .scale(0.3, 0.3, 0.3)
118
+ .translate(+1.6, +0.4, -0.8)
119
+ .scale(0.4, 0.4, 0.4)
113
120
  .rotate_y(-Math::PI / 8),
114
121
  )
115
122
 
116
- cube_r = Ra::Shape::Cube.new(
123
+ cylinder = Ra::Shape::Cylinder.new(
117
124
  material: Ra::Material.new(
118
125
  base: Ra::Pattern::Gradient.new(
119
126
  color_a: Ra::Color.hex('#84cc16'),
120
127
  color_b: Ra::Color.hex('#f97316'),
121
128
  ),
122
- reflective: 0.5,
129
+ reflective: 0.3,
123
130
  ),
124
131
  transform: Ra::Transform
125
- .translate(-1.0, +0.3, -1.0)
126
- .scale(0.3, 0.3, 0.3)
132
+ .translate(-1.6, +0.4, -0.8)
133
+ .scale(0.4, 0.4, 0.4)
127
134
  .rotate_y(+Math::PI / 8),
128
135
  )
129
136
 
@@ -134,15 +141,15 @@ lights = [
134
141
 
135
142
  shapes = [
136
143
  floor,
144
+ ceiling,
137
145
  wall_l,
138
146
  wall_r,
139
147
  sphere,
140
- cube_l,
141
- cube_r,
148
+ cube,
149
+ cylinder,
142
150
  ].freeze
143
151
 
144
152
  world = Ra::World.new(lights:, shapes:)
145
153
  engine = Ra::Engine.new(camera:, world:)
146
- canvas = engine.render
147
154
 
148
- Ra.logger.log(canvas.ppm)
155
+ engine.ppm { |text| Ra.logger.log(text) }
data/lib/ra/camera.rb CHANGED
@@ -24,15 +24,31 @@ module Ra
24
24
  # dimensions rays are cast to the center of pixels evenly distrubted across
25
25
  # the screen.
26
26
  class Camera
27
- attr_accessor :h, :w, :fov, :transform
27
+ include Enumerable
28
+
29
+ # @!attribute h
30
+ # @return [Integer]
31
+ attr_accessor :h
32
+
33
+ # @!attribute w
34
+ # @return [Integer]
35
+ attr_accessor :w
36
+
37
+ # @!attribute fov
38
+ # @return [Numeric]
39
+ attr_accessor :fov
40
+
41
+ # @!attribute transform
42
+ # @return [Ra::Transform]
43
+ attr_accessor :transform
28
44
 
29
45
  DEFAULT_W = 1280
30
46
  DEFAULT_H = 1024
31
47
  DEFAULT_FOV = Math::PI / 3
32
48
 
33
49
  # @param transform [Ra::Transform]
34
- # @param h [Numeric]
35
- # @param w [Numeric]
50
+ # @param h [Integer]
51
+ # @param w [Integer]
36
52
  # @param fov [Numeric]
37
53
  def initialize(transform: Transform::IDENTITY, h: DEFAULT_H, w: DEFAULT_W, fov: DEFAULT_FOV)
38
54
  @transform = transform
@@ -41,6 +57,22 @@ module Ra
41
57
  @fov = fov
42
58
  end
43
59
 
60
+ # @yield [y, x, ray] y, x, ray
61
+ # @yieldparam [Integer] y
62
+ # @yieldparam [Integer] x
63
+ # @yieldparam [Ra::Ray] ray
64
+ def each
65
+ @h.times do |y|
66
+ @w.times do |x|
67
+ ray = ray(
68
+ y:,
69
+ x:,
70
+ )
71
+ yield(y, x, ray)
72
+ end
73
+ end
74
+ end
75
+
44
76
  # @param x [Numeric]
45
77
  # @param y [Numeric]
46
78
  # @return [Ra::Ray]
data/lib/ra/color.rb CHANGED
@@ -17,7 +17,17 @@ module Ra
17
17
  # )
18
18
  # color.ppm == "128 179 230"
19
19
  class Color
20
- attr_accessor :r, :g, :b
20
+ # @!attribute r
21
+ # @return [Numeric]
22
+ attr_accessor :r
23
+
24
+ # @!attribute g
25
+ # @return [Numeric]
26
+ attr_accessor :g
27
+
28
+ # @!attribute b
29
+ # @return [Numeric]
30
+ attr_accessor :b
21
31
 
22
32
  PRECISION = 255
23
33
 
data/lib/ra/engine.rb CHANGED
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ra
4
- # An engine takes uses a world / camera to generate a canvas.
4
+ # An engine takes a world / camera and generates a PPM.
5
5
  class Engine
6
+ include Enumerable
7
+
6
8
  PRECISION = 255
9
+ PORCESSES = 8
10
+
11
+ PPM_VERSION = 'P3'
12
+ PPM_DEFAULT = '0 0 0'
7
13
 
8
14
  # @param world [Ra::World]
9
15
  # @param camera [Ra::Camera]
@@ -12,26 +18,27 @@ module Ra
12
18
  @camera = camera
13
19
  end
14
20
 
15
- # @return [Ra::Canvas]
16
- def render
17
- Ra::Canvas.new(w: @camera.w, h: @camera.h, precision: PRECISION).tap do |canvas|
18
- @camera.h.times do |y|
19
- @camera.w.times do |x|
20
- draw(x:, y:, canvas:)
21
- end
22
- end
21
+ # @yield [pixel]
22
+ # @yieldparam [Ra::Pixel] pixel
23
+ def each
24
+ @camera.each do |y, x, ray|
25
+ color = @world.color(ray:)
26
+ yield(Ra::Pixel.new(x:, y:, color:))
23
27
  end
24
28
  end
25
29
 
26
- private
30
+ # @yield [text]
31
+ # @yieldparam [String] text
32
+ def ppm
33
+ yield(<<~PPM)
34
+ #{PPM_VERSION}
35
+ #{@camera.w} #{@camera.h}
36
+ #{Color::PRECISION}
37
+ PPM
27
38
 
28
- # @param x [Integer]
29
- # @param y [Integer]
30
- # @param canvas [Ra::Canvas]
31
- def draw(x:, y:, canvas:)
32
- ray = @camera.ray(x:, y:)
33
-
34
- canvas[x, y] = @world.color(ray:)
39
+ each do |pixel|
40
+ yield(pixel.color ? pixel.color.ppm : PPM_DEFAULT)
41
+ end
35
42
  end
36
43
  end
37
44
  end
@@ -3,7 +3,17 @@
3
3
  module Ra
4
4
  # An intersection tracks the time t at which a shape is intersected by a ray.
5
5
  class Intersection
6
- attr_accessor :t, :ray, :shape
6
+ # @!attribute t
7
+ # @return [Numeric]
8
+ attr_accessor :t
9
+
10
+ # @attribute ray
11
+ # @return [Ra::Ray]
12
+ attr_accessor :ray
13
+
14
+ # @attribute shape
15
+ # @return [Ra::Shape::Base]
16
+ attr_accessor :shape
7
17
 
8
18
  # @param intersections Array<Ra::Intersection>
9
19
  # @return [Ra::Intersection, nil]
@@ -42,5 +52,25 @@ module Ra
42
52
 
43
53
  Surface.new(shape:, eyev:, normalv:, reflectv:, point:)
44
54
  end
55
+
56
+ # @return [Vector]
57
+ def position
58
+ @position ||= @ray.position(t: @t)
59
+ end
60
+
61
+ # @return [Numeric]
62
+ def x
63
+ position[0]
64
+ end
65
+
66
+ # @return [Numeric]
67
+ def y
68
+ position[1]
69
+ end
70
+
71
+ # @return [Numeric]
72
+ def z
73
+ position[1]
74
+ end
45
75
  end
46
76
  end
data/lib/ra/light.rb CHANGED
@@ -9,7 +9,13 @@ module Ra
9
9
  # position: Vector[0, 0, 0, Ra::Tuple::POINT],
10
10
  # )
11
11
  class Light
12
- attr_accessor :intensity, :position
12
+ # @!attribute intensity
13
+ # @return [Ra::Color]
14
+ attr_accessor :intensity
15
+
16
+ # @!attribute position
17
+ # @return [Vector]
18
+ attr_accessor :position
13
19
 
14
20
  # @param intensity [Ra::Color]
15
21
  # @param position [Vector]
data/lib/ra/lighting.rb CHANGED
@@ -3,7 +3,17 @@
3
3
  module Ra
4
4
  # Lighting encaspulates a [Phong Reflection Model](https://en.wikipedia.org/wiki/phong_reflection_model).
5
5
  class Lighting
6
- attr_accessor :light, :surface, :shadowed
6
+ # @!attribute surface
7
+ # @return [Ra::Surface]
8
+ attr_accessor :surface
9
+
10
+ # @!attribute shadowed
11
+ # @return [Boolean]
12
+ attr_accessor :shadowed
13
+
14
+ # @!attribute light
15
+ # @return [Ra::Light]
16
+ attr_accessor :light
7
17
 
8
18
  # @param light [Ra::Light]
9
19
  # @param surface [Ra::Surface]
data/lib/ra/material.rb CHANGED
@@ -11,14 +11,36 @@ module Ra
11
11
  # shininess: 200,
12
12
  # )
13
13
  class Material
14
- attr_accessor :base, :ambient, :diffuse, :reflective, :specular, :shininess
14
+ # @!attribute base
15
+ # @return [Ra::Color, Ra::Pattern::Base]
16
+ attr_accessor :base
17
+
18
+ # @!attribute ambient
19
+ # @return [Float]
20
+ attr_accessor :ambient
21
+
22
+ # @!attribute diffuse
23
+ # @return [Float]
24
+ attr_accessor :diffuse
25
+
26
+ # @!attribute reflective
27
+ # @return [Float]
28
+ attr_accessor :reflective
29
+
30
+ # @!attribute specular
31
+ # @return [Float]
32
+ attr_accessor :specular
33
+
34
+ # @!attribute shininess
35
+ # @return [Integer]
36
+ attr_accessor :shininess
15
37
 
16
38
  # @param base [Ra::Color, Ra::Pattern:::Base]
17
39
  # @param ambient [Float] between 0.0 and 1.0
18
40
  # @param diffuse [Float] between 0.0 and 1.0
19
41
  # @param reflective [Float] between 0.0 and 1.0
20
42
  # @param specular [Float] between 0.0 and 1.0
21
- # @param shininess [Numeric]
43
+ # @param shininess [Integer]
22
44
  def initialize(base:, ambient: 0.0, diffuse: 0.8, reflective: 0.0, specular: 0.2, shininess: 80)
23
45
  raise ArgumentError, "ambient=#{ambient} must be between 0 and 1" unless ambient.between?(0, 1)
24
46
  raise ArgumentError, "ambient=#{diffuse} must be between 0 and 1" unless diffuse.between?(0, 1)
data/lib/ra/pixel.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A pixel is a point in an image. It pairs the coordinates (x,y) with the color for that point.
5
+ class Pixel
6
+ # @!attribute x
7
+ # @return [Integer]
8
+ attr_accessor :x
9
+
10
+ # @!attribute y
11
+ # @return [Integer]
12
+ attr_accessor :y
13
+
14
+ # @!attribute color
15
+ # @return [Ra::Color]
16
+ attr_accessor :color
17
+
18
+ # @param x [Integer]
19
+ # @param y [Integer]
20
+ # @param color [Ra::Color]
21
+ def initialize(x:, y:, color:)
22
+ @x = x
23
+ @y = y
24
+ @color = color
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A solver for all things quadratic. Given the equation:
5
+ #
6
+ # ax² + bx +c = 0
7
+ #
8
+ # The solution for `x`` can be found via:
9
+ #
10
+ # (-b ± √(b² - 4ac)) / (2a)
11
+ #
12
+ # No solution is defined when the discriminant (`b² - 4ac`) is negative or `a` is zero.
13
+ module Quadratic
14
+ # (-b ± √(b² - 4ac)) / (2a)
15
+ #
16
+ # @param a [Numeric]
17
+ # @param b [Numeric]
18
+ # @param c [Numeric]
19
+ # @return [Array<Numeric>]
20
+ def self.solve(a:, b:, c:)
21
+ return [] if a.zero?
22
+
23
+ discriminant = (b**2) - (4 * a * c)
24
+ return [] if discriminant.negative?
25
+
26
+ [
27
+ (-b - Math.sqrt(discriminant)) / (2 * a),
28
+ (-b + Math.sqrt(discriminant)) / (2 * a),
29
+ ]
30
+ end
31
+ end
32
+ end
data/lib/ra/ray.rb CHANGED
@@ -21,7 +21,13 @@ module Ra
21
21
  # )
22
22
  # ray.transform(transform: Ra::Transform.scale(1, 2, 3))
23
23
  class Ray
24
- attr_accessor :origin, :direction
24
+ # @!attribute origin
25
+ # @return [Vector]
26
+ attr_accessor :origin
27
+
28
+ # @!attribute direction
29
+ # @return [Vector]
30
+ attr_accessor :direction
25
31
 
26
32
  # @param origin [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::POINT]
27
33
  # @param direction [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::VECTOR]
@@ -49,5 +55,90 @@ module Ra
49
55
  def ==(other)
50
56
  origin == other.origin && direction == other.direction
51
57
  end
58
+
59
+ # @param t [Numeric]
60
+ # @return [Numeric]
61
+ def x(t:)
62
+ origin_x + (direction_x * t)
63
+ end
64
+
65
+ # @param t [Numeric]
66
+ # @return [Numeric]
67
+ def y(t:)
68
+ origin_y + (direction_y * t)
69
+ end
70
+
71
+ # @param t [Numeric]
72
+ # @return [Numeric]
73
+ def z(t:)
74
+ origin_z + (direction_z * t)
75
+ end
76
+
77
+ # The time t when the ray is at x
78
+ # @param x [Numeric]
79
+ # @return [Numeric]
80
+ def t_x(x)
81
+ return if direction_x.zero?
82
+
83
+ (x - origin_x) / direction_x
84
+ end
85
+
86
+ # The time t when the ray is at y
87
+ # @param y [Numeric]
88
+ # @return [Numeric, nil]
89
+ def t_y(y)
90
+ return if direction_y.zero?
91
+
92
+ (y - origin_y) / direction_y
93
+ end
94
+
95
+ # The time t when the ray is at z
96
+ # @param z [Numeric]
97
+ # @return [Numeric, nil]
98
+ def t_z(z)
99
+ return if direction_z.zero?
100
+
101
+ (z - origin_z) / direction_z
102
+ end
103
+
104
+ # @return [Numeric]
105
+ def direction_x
106
+ @direction[0]
107
+ end
108
+
109
+ # @return [Numeric]
110
+ def direction_y
111
+ @direction[1]
112
+ end
113
+
114
+ # @return [Numeric]
115
+ def direction_z
116
+ @direction[2]
117
+ end
118
+
119
+ # @return [0] 0 = vector / 1 = point
120
+ def direction_w
121
+ @direction[3]
122
+ end
123
+
124
+ # @return [Numeric]
125
+ def origin_x
126
+ @origin[0]
127
+ end
128
+
129
+ # @return [Numeric]
130
+ def origin_y
131
+ @origin[1]
132
+ end
133
+
134
+ # @return [Numeric]
135
+ def origin_z
136
+ @origin[2]
137
+ end
138
+
139
+ # @return [1] 0 = vector / 1 = point
140
+ def origin_w
141
+ @origin[3]
142
+ end
52
143
  end
53
144
  end
data/lib/ra/shape/base.rb CHANGED
@@ -6,6 +6,8 @@ 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
+ # @!attribute material
10
+ # @return [Ra::Material]
9
11
  attr_accessor :material
10
12
 
11
13
  # @param material [Ra::Material]
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Shape
5
+ # A cylinder at origin <0,0,0> with a radius 1. A cylinder surface is defined:
6
+ #
7
+ # (x² + z² = radius² AND y BETWEEN ±1) OR (y = ±1 AND x² + z² < radius)
8
+ #
9
+ # A ray `x` / `y` / `z` values at `t` use the `origin` and `direction`:
10
+ #
11
+ # x = origin.x + direction.x * t
12
+ # y = origin.y + direction.y * t
13
+ # z = origin.z + direction.z * t
14
+ #
15
+ # Substituting `x` / `y` / `z` allows for solving for `t`:
16
+ #
17
+ # (origin.x + direction.x * t)² + (origin.z + direction.z * t)² = 1
18
+ #
19
+ # Simplifying gives a quadratic formula with terms defined as:
20
+ #
21
+ # a = direction.x² + direction.z²
22
+ # b = 2 * ((origin.x * direction.x) + (origin.z * direction.z))
23
+ # c = origin.x² + origin.z² - 1
24
+ # discriminant = b² - 4ac
25
+ # t = (-b ± √discriminant) / (2a)
26
+ #
27
+ # A discriminant <0 indicates the ray does not intersect the sphere.
28
+ class Cylinder < Base
29
+ MIN_Y = -1
30
+ MAX_Y = +1
31
+ RADIUS = 1
32
+
33
+ # @param point [Vector] <x, y, z, Tuple::POINT>
34
+ # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
35
+ def uv_point(point:)
36
+ x = point[0]
37
+ y = point[1]
38
+ z = point[2]
39
+
40
+ theta = Math.atan2(x, z)
41
+
42
+ u = 1 - ((theta / (2 * Math::PI)) + 0.5)
43
+ v = y % 1.0
44
+
45
+ Vector[u, v]
46
+ end
47
+
48
+ # @param ray [Ra::Ray] local
49
+ # @return [Array<Numeric>]
50
+ def t_intersect(ray:)
51
+ t_intersect_caps(ray:) +
52
+ t_intersect_side(ray:).filter { |t| ray.y(t:) > MIN_Y && ray.y(t:) < MAX_Y }
53
+ end
54
+
55
+ # @param point [Vector]
56
+ # @return [Ra::Tuple]
57
+ def l_normal(point:)
58
+ x = point[0]
59
+ y = point[1]
60
+ z = point[2]
61
+
62
+ distance = (x * x) + (z * z)
63
+
64
+ if distance < 1
65
+ return Vector[0, +1, 0, Tuple::VECTOR] if y >= MAX_Y - EPSILON
66
+ return Vector[0, -1, 0, Tuple::VECTOR] if y <= MIN_Y + EPSILON
67
+ end
68
+
69
+ Vector[point[0], 0, point[2], Tuple::VECTOR]
70
+ end
71
+
72
+ private
73
+
74
+ # @param ray [Ra::Ray] local
75
+ # @return [Array<Numeric>]
76
+ def t_intersect_side(ray:)
77
+ direction_x = ray.direction_x
78
+ direction_z = ray.direction_z
79
+ origin_x = ray.origin_x
80
+ origin_z = ray.origin_z
81
+
82
+ Quadratic.solve(
83
+ a: (direction_x * direction_x) + (direction_z * direction_z),
84
+ b: 2 * ((origin_x * direction_x) + (origin_z * direction_z)),
85
+ c: ((origin_x * origin_x) + (origin_z * origin_z)) - RADIUS,
86
+ )
87
+ end
88
+
89
+ def t_intersect_caps(ray:)
90
+ [
91
+ t_intersect_y(ray:, y: MIN_Y),
92
+ t_intersect_y(ray:, y: MAX_Y),
93
+ ].compact
94
+ end
95
+
96
+ # @param ray [Ra::Ray] local
97
+ # @return [Numeric, nil]
98
+ def t_intersect_y(ray:, y:)
99
+ t = ray.t_y(y)
100
+ return unless t
101
+
102
+ point = ray.position(t:)
103
+
104
+ return if (point[0] * point[0]) + (point[2] * point[2]) > RADIUS
105
+
106
+ t
107
+ end
108
+ end
109
+ end
110
+ end
@@ -30,8 +30,8 @@ module Ra
30
30
  # @param ray [Ra::Ray] local
31
31
  # @return [Array<Numeric>]
32
32
  def t_intersect(ray:)
33
- origin_y = ray.origin[1]
34
- direction_y = ray.direction[1]
33
+ origin_y = ray.origin_y
34
+ direction_y = ray.direction_y
35
35
 
36
36
  return [] if direction_y.abs < EPSILON
37
37
 
@@ -32,6 +32,8 @@ module Ra
32
32
  #
33
33
  # A discriminant <0 indicates the ray does not intersect the sphere.
34
34
  class Sphere < Base
35
+ RADIUS = 1
36
+
35
37
  # @param point [Vector] <x, y, z, Tuple::POINT>
36
38
  # @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
37
39
  def uv_point(point:)
@@ -55,10 +57,10 @@ module Ra
55
57
  origin = ray.origin - Vector[0, 0, 0, Ra::Tuple::POINT]
56
58
  direction = ray.direction
57
59
 
58
- quadratic(
60
+ Quadratic.solve(
59
61
  a: direction.dot(direction),
60
62
  b: 2 * direction.dot(origin),
61
- c: origin.dot(origin) - 1,
63
+ c: origin.dot(origin) - RADIUS,
62
64
  )
63
65
  end
64
66
 
@@ -67,24 +69,6 @@ module Ra
67
69
  def l_normal(point:)
68
70
  point - Vector[0, 0, 0, Ra::Tuple::VECTOR]
69
71
  end
70
-
71
- private
72
-
73
- # (-b ± √(b² - 4ac)) / (2a)
74
- #
75
- # @param a [Numeric]
76
- # @param b [Numeric]
77
- # @param c [Numeric]
78
- # @return [Array<Numeric>]
79
- def quadratic(a:, b:, c:)
80
- discriminant = (b**2) - (4 * a * c)
81
- return [] if discriminant.negative?
82
-
83
- [
84
- (-b - Math.sqrt(discriminant)) / (2 * a),
85
- (-b + Math.sqrt(discriminant)) / (2 * a),
86
- ]
87
- end
88
72
  end
89
73
  end
90
74
  end
data/lib/ra/surface.rb CHANGED
@@ -3,7 +3,25 @@
3
3
  module Ra
4
4
  # A surface contains everything needed to apply lighting.
5
5
  class Surface
6
- attr_accessor :eyev, :normalv, :reflectv, :shape, :point
6
+ # @!attribute eyev
7
+ # @return [Vector]
8
+ attr_accessor :eyev
9
+
10
+ # @!attribute normalv
11
+ # @return [Vector]
12
+ attr_accessor :normalv
13
+
14
+ # @!attribute reflectv
15
+ # @return [Vector]
16
+ attr_accessor :reflectv
17
+
18
+ # @!attribute shape
19
+ # @return [Ra::Shape]
20
+ attr_accessor :shape
21
+
22
+ # @!attribute point
23
+ # @return [Vector]
24
+ attr_accessor :point
7
25
 
8
26
  # @param eyev [Vector]
9
27
  # @param normalv [Vector]
data/lib/ra/transform.rb CHANGED
@@ -94,34 +94,34 @@ module Ra
94
94
  # @param x [Numeric]
95
95
  # @param y [Numeric]
96
96
  # @param z [Numeric]
97
- def translate(...)
98
- self * self.class.translate(...)
97
+ def translate(x, y, z)
98
+ self * self.class.translate(x, y, z)
99
99
  end
100
100
 
101
101
  # @return [Ra::Transform]
102
102
  # @param x [Numeric]
103
103
  # @param y [Numeric]
104
104
  # @param z [Numeric]
105
- def scale(...)
106
- self * self.class.scale(...)
105
+ def scale(x, y, z)
106
+ self * self.class.scale(x, y, z)
107
107
  end
108
108
 
109
109
  # @return [Ra::Transform]
110
110
  # @param rotation [Numeric]
111
- def rotate_x(...)
112
- self * self.class.rotate_x(...)
111
+ def rotate_x(rotation)
112
+ self * self.class.rotate_x(rotation)
113
113
  end
114
114
 
115
115
  # @return [Ra::Transform]
116
116
  # @param rotation [Numeric]
117
- def rotate_y(...)
118
- self * self.class.rotate_y(...)
117
+ def rotate_y(rotation)
118
+ self * self.class.rotate_y(rotation)
119
119
  end
120
120
 
121
121
  # @return [Ra::Transform]
122
122
  # @param rotation [Numeric]
123
- def rotate_z(...)
124
- self * self.class.rotate_z(...)
123
+ def rotate_z(rotation)
124
+ self * self.class.rotate_z(rotation)
125
125
  end
126
126
 
127
127
  # Avoid re-computing a transform inverse by memoizing.
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.7.0'
4
+ VERSION = '1.0.0'
5
5
  end
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.7.0
4
+ version: 1.0.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-23 00:00:00.000000000 Z
11
+ date: 2024-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: matrix
@@ -79,7 +79,6 @@ files:
79
79
  - exe/ra
80
80
  - lib/ra.rb
81
81
  - lib/ra/camera.rb
82
- - lib/ra/canvas.rb
83
82
  - lib/ra/color.rb
84
83
  - lib/ra/engine.rb
85
84
  - lib/ra/intersection.rb
@@ -93,9 +92,12 @@ files:
93
92
  - lib/ra/pattern/rings.rb
94
93
  - lib/ra/pattern/stripes.rb
95
94
  - lib/ra/pattern/texture.rb
95
+ - lib/ra/pixel.rb
96
+ - lib/ra/quadratic.rb
96
97
  - lib/ra/ray.rb
97
98
  - lib/ra/shape/base.rb
98
99
  - lib/ra/shape/cube.rb
100
+ - lib/ra/shape/cylinder.rb
99
101
  - lib/ra/shape/plane.rb
100
102
  - lib/ra/shape/sphere.rb
101
103
  - lib/ra/surface.rb
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  - !ruby/object:Gem::Version
124
126
  version: '0'
125
127
  requirements: []
126
- rubygems_version: 3.4.19
128
+ rubygems_version: 3.5.23
127
129
  signing_key:
128
130
  specification_version: 4
129
131
  summary: A graphics library written for fun.
data/lib/ra/canvas.rb DELETED
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ra
4
- # A canvas is used to track pixels at <x,y> coordinates. It is given a w / h
5
- # and on initialization allocates a w by h collection of pixels. For example,
6
- # defining a basic black and white checkboard canvas with altering squares
7
- # then saving as [PPM](https://netpbm.sourceforge.net/doc/ppm.html):
8
- #
9
- # canvas = Ra::Canvas.new(w: 4, h: 5, precision: 15)
10
- # canvas.w.times do |x|
11
- # canvas.h.times do |y|
12
- # canvas[x,y] = (x + y) % 2 == 0 ? Ra::Color.black : Ra::Color.white
13
- # end
14
- # end
15
- # canvas.ppm
16
- class Canvas
17
- attr_accessor :w, :h, :precision
18
-
19
- PPM_VERSION = 'P3'
20
-
21
- # @param w [Integer]
22
- # @param h [Integer]
23
- # @param precision [Integer]
24
- def initialize(w:, h:, precision: Color::PRECISION)
25
- @w = w
26
- @h = h
27
- @precision = precision
28
- @pixels = Array.new(w) { Array.new(h) }
29
- end
30
-
31
- # @param x [Integer]
32
- # @param y [Integer]
33
- def [](x, y)
34
- raise ArgumentError, "x=#{x} cannot be negative" if x.negative?
35
- raise ArgumentError, "y=#{y} cannot be negative" if y.negative?
36
- raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
37
- raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
38
-
39
- @pixels[x][y] || Color.black
40
- end
41
-
42
- # @param x [Integer]
43
- # @param y [Integer]
44
- # @param color [Ra::Color]
45
- def []=(x, y, color)
46
- raise ArgumentError, "x=#{x} cannot be negative" if x.negative?
47
- raise ArgumentError, "y=#{y} cannot be negative" if y.negative?
48
- raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
49
- raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
50
-
51
- @pixels[x][y] = color
52
- end
53
-
54
- # @return [String]
55
- def ppm
56
- buffer = String.new(<<~PPM, encoding: 'ascii')
57
- #{PPM_VERSION}
58
- #{@w} #{@h}
59
- #{@precision}
60
- PPM
61
-
62
- @h.times do |y|
63
- @w.times do |x|
64
- buffer << (self[x, y].ppm(precision: @precision)) << "\n"
65
- end
66
- end
67
-
68
- buffer
69
- end
70
- end
71
- end