ra 0.7.0 → 1.0.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: 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