ra 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ra/logger.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A logger used to make exe testing possible.
5
+ #
6
+ # logger = Logger.new
7
+ # logger.log("Greetings!")
8
+ class Logger
9
+ # @param stream [IO]
10
+ def initialize(stream: $stdout)
11
+ @stream = stream
12
+ end
13
+
14
+ # @param message [String]
15
+ def log(message = nil)
16
+ @stream.puts message
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A material is used to define the properties of an object that impact the color applied. For example:
5
+ #
6
+ # material = Ra::Material.new(
7
+ # base: Ra::Color.uniform(0.5),
8
+ # ambient: 0.2,
9
+ # diffuse: 0.5,
10
+ # specular: 0.7,
11
+ # shininess: 200,
12
+ # )
13
+ class Material
14
+ attr_accessor :base, :ambient, :diffuse, :specular, :shininess
15
+
16
+ # @param base [Ra::Color, Ra::Pattern:::Base]
17
+ # @param ambient [Float] between 0.0 and 1.0
18
+ # @param diffuse [Float] between 0.0 and 1.0
19
+ # @param specular [Float] between 0.0 and 1.0
20
+ # @param shininess [Numeric]
21
+ def initialize(base:, ambient: 0.0, diffuse: 0.8, specular: 0.2, shininess: 80)
22
+ @base = base
23
+ @ambient = ambient
24
+ @diffuse = diffuse
25
+ @specular = specular
26
+ @shininess = shininess
27
+ end
28
+
29
+ # @param point [Vector]
30
+ # @return [Ra::Color]
31
+ def color(point:)
32
+ return base if base.is_a?(Color)
33
+
34
+ base.color(point:)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # An abstract pattern. Any concrete subclass of pattern must implement the method `color`.
6
+ class Base
7
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
8
+ # @return [Ra::Color]
9
+ def color(point:)
10
+ raise NotImplementedError, '#color must be implemented by a concrete subclass'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A checkers pattern that alternates colors using:
6
+ #
7
+ # colors[⌊u * rows⌋ + ⌊v * cols)⌋ % colors.count]
8
+ class Checkers < Base
9
+ DEFAULT_ROWS = 2
10
+ DEFAULT_COLS = 2
11
+ DEFAULT_COLORS = [
12
+ Color.black,
13
+ Color.white,
14
+ ].freeze
15
+
16
+ # @param rows [Integer]
17
+ # @param cols [Integer]
18
+ # @param colors [Array<Ra::Color>]
19
+ def initialize(cols: DEFAULT_COLS, rows: DEFAULT_ROWS, colors: DEFAULT_COLORS)
20
+ super()
21
+ @rows = rows
22
+ @cols = cols
23
+ @colors = colors
24
+ end
25
+
26
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
27
+ # @return [Ra::Color]
28
+ def color(point:)
29
+ u = point[0]
30
+ v = point[1]
31
+ index = (u * @rows).floor + (v * @cols).floor
32
+
33
+ @colors[index % @colors.count]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A graident pattern from `color_a` to `color_b` using:
6
+ #
7
+ # color_b + (color_b - color_a) * (u + v) / 2
8
+ class Gradient < Base
9
+ # @param color_a [Ra::Color]
10
+ # @param color_b [Ra::Color]
11
+ def initialize(color_a:, color_b:)
12
+ super()
13
+ @color_a = color_a
14
+ @color_b = color_b
15
+ end
16
+
17
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
18
+ # @return [Ra::Color]
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)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A rings pattern that alternates colors using:
6
+ #
7
+ # colors[⌊√(u² + v²)⌋]
8
+ class Rings < Base
9
+ # @param colors [Array<Ra::Color>]
10
+ def initialize(colors:)
11
+ super()
12
+ @colors = colors
13
+ end
14
+
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
21
+
22
+ @colors[index % @colors.count]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A stripe pattern that alternates colors using:
6
+ #
7
+ # colors[⌊point.x⌋]
8
+ class Stripes < Base
9
+ # @param colors [Array<Ra::Color>]
10
+ def initialize(colors:)
11
+ super()
12
+ @colors = colors
13
+ end
14
+
15
+ # @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
16
+ # @return [Ra::Color]
17
+ def color(point:)
18
+ count = @colors.count
19
+ u = point[0]
20
+ v = point[1]
21
+ value = (u + v) * (2 * count)
22
+
23
+ @colors[value.floor % count]
24
+ end
25
+ end
26
+ end
27
+ 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/ray.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A ray is positioned at an origin travelling by a direction. A ray is cast and used to identify collisions with
5
+ # objects. For example:
6
+ #
7
+ # ray = Ra::Ray.new(
8
+ # origin: Vector[0, 0, 0, Ra::Tuple::POINT],
9
+ # direction: Vector[1, 2, 3, Ra::Tuple::VECTOR],
10
+ # )
11
+ # ray.position(t: 1) == Vector[1, 2, 3, Ra::Tuple::VECTOR]
12
+ # ray.position(t: 2) == Vector[2, 4, 6, Ra::Tuple::VECTOR]
13
+ # ray.position(t: 3) == Vector[3, 6, 9, Ra::Tuple::VECTOR]
14
+ #
15
+ # A ray can be transformed. This is useful when considering the ray relative to an object that has a transform
16
+ # associated with it. For example:
17
+ #
18
+ # ray = Ra::Ray.new(
19
+ # origin: Vector[0, 0, 0, Ra::Tuple::POINT],
20
+ # direction: Vector[1, 2, 3, Ra::Tuple::VECTOR],
21
+ # )
22
+ # ray.transform(transform: Ra::Transform.scale(1, 2, 3))
23
+ class Ray
24
+ attr_accessor :origin, :direction
25
+
26
+ # @param origin [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::POINT]
27
+ # @param direction [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::VECTOR]
28
+ def initialize(origin:, direction:)
29
+ @origin = origin
30
+ @direction = direction
31
+ end
32
+
33
+ # @param t [Numeric]
34
+ # @return [Vector]
35
+ def position(t:)
36
+ @origin + (@direction * t)
37
+ end
38
+
39
+ # @param transform [Ra::Transform]
40
+ # @return [Ra::Ray]
41
+ def transform(transform)
42
+ self.class.new(
43
+ origin: transform * @origin,
44
+ direction: transform * @direction,
45
+ )
46
+ end
47
+
48
+ # @return [Boolean]
49
+ def ==(other)
50
+ origin == other.origin && direction == other.direction
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Shape
5
+ # An abstract shape. Any concrete subclass of shape must implement the
6
+ # methods `l_normal` and `t_intersect`. Both methods use a point / ray
7
+ # with a local transform applied.
8
+ class Base
9
+ attr_accessor :material
10
+
11
+ # @param material [Ra::Material]
12
+ # @param transform [Ra::Matrix]
13
+ def initialize(material:, transform: Transform::IDENTITY)
14
+ @material = material
15
+ @transform = transform
16
+ end
17
+
18
+ # @param ray [Ra::Ray]
19
+ # @return [Array<Ra::Intersection>]
20
+ def intersect(ray:)
21
+ t_intersect(ray: ray.transform(@transform.inverse))
22
+ .map { |t| Ra::Intersection.new(ray:, shape: self, t:) }
23
+ end
24
+
25
+ # @param point [Vector] <x, y, z, Tuple::POINT>
26
+ # @return [Vector] <x, y, z, Tuple::POINT>
27
+ def normal(point:)
28
+ normal = @transform.inverse.transpose * l_normal(point: @transform.inverse * point)
29
+
30
+ Vector[normal[0], normal[1], normal[2], Ra::Tuple::VECTOR].normalize
31
+ end
32
+
33
+ # @param point [Vector] <x, y, z, Tuple::POINT>
34
+ # @return [Color]
35
+ def color(point:)
36
+ @material.color(point: uv_point(point: @transform.inverse * point))
37
+ end
38
+
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
44
+
45
+ # @param ray [Ra::Ray] local
46
+ # @return [Array<Intersection>]
47
+ def t_intersect(ray:)
48
+ raise NotImplementedError, '#t_intersect must be implemented by a concrete subclass'
49
+ end
50
+
51
+ # @param point [Vector] local
52
+ # @return [Vector]
53
+ def l_normal(point:)
54
+ raise NotImplementedError, '#l_normal must be implemented by a concrete subclass'
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Shape
5
+ # A cube centered at <0,0,0> with sides of l=2. A cube surface is defined:
6
+ #
7
+ # x between (-1..+1)
8
+ # y between (-1..+1)
9
+ # z between (-1..+1)
10
+ # x = ±1 OR y = ±1 OR z = ±1
11
+ #
12
+ # A ray `x` / `y` / `z` values at `t` use the `origin` and `direction`:
13
+ #
14
+ # x = origin.x + direction.x * t
15
+ # y = origin.y + direction.y * t
16
+ # z = origin.z + direction.z * t
17
+ #
18
+ # The ray therefore may intersect when:
19
+ #
20
+ # origin.x + direction.x * t = ±1 OR origin.y + direction.y * t = ±1 OR origin.z + direction.z * t = ±1
21
+ #
22
+ # Thus 6 planes can be checked for intersect.
23
+ class Cube < Base
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
38
+
39
+ # @param ray [Ra::Ray] local
40
+ # @return [Array<Numeric>]
41
+ def t_intersect(ray:)
42
+ t_min = t_min(ray:)
43
+ t_max = t_max(ray:)
44
+
45
+ return [] if !t_min || !t_max || t_min > t_max
46
+
47
+ [
48
+ t_min,
49
+ t_max,
50
+ ]
51
+ end
52
+
53
+ # @param point [Vector]
54
+ # @return [Ra::Tuple]
55
+ def l_normal(point:)
56
+ x = point[0].abs
57
+ y = point[1].abs
58
+ z = point[2].abs
59
+
60
+ Vector[
61
+ (is_x = x > y && x > z) ? 1 : 0,
62
+ (is_y = y > x && y > z) ? 1 : 0,
63
+ is_x || is_y ? 0 : 1,
64
+ Ra::Tuple::VECTOR
65
+ ]
66
+ end
67
+
68
+ private
69
+
70
+ # @param ray [Ra::Ray]
71
+ # @return [Integer]
72
+ def t_min(ray:)
73
+ (0..2).map { |i| t_min_max(ray.origin[i], ray.direction[i]).min }.max
74
+ end
75
+
76
+ # @param ray [Ra::Ray]
77
+ # @return [Integer]
78
+ def t_max(ray:)
79
+ (0..2).map { |i| t_min_max(ray.origin[i], ray.direction[i]).max }.min
80
+ end
81
+
82
+ # @param origin [Numeric]
83
+ # @param direction [Numeric]
84
+ # @return [Array<Numeric,Numeric>]
85
+ def t_min_max(origin, direction)
86
+ t_min_numerator = -1 - origin
87
+ t_max_numerator = +1 - origin
88
+
89
+ if direction.abs < EPSILON
90
+ t_min = t_min_numerator * Float::INFINITY
91
+ t_max = t_max_numerator * Float::INFINITY
92
+ else
93
+ t_min = t_min_numerator / direction
94
+ t_max = t_max_numerator / direction
95
+ end
96
+
97
+ t_min < t_max ? [t_min, t_max] : [t_max, t_min]
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
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Shape
5
+ # A plane for all x / z where y = 0. A plane surface is defined:
6
+ #
7
+ # y = 0
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
+ # Therefore, a plane has a single intersection at:
16
+ #
17
+ # t = -origin.y / direction.y
18
+ #
19
+ # A direction.y < EPISLON indicates the ray does not intersect the plane.
20
+ class Plane < Base
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
29
+
30
+ # @param ray [Ra::Ray] local
31
+ # @return [Array<Numeric>]
32
+ def t_intersect(ray:)
33
+ origin_y = ray.origin[1]
34
+ direction_y = ray.direction[1]
35
+
36
+ return [] if direction_y.abs < EPSILON
37
+
38
+ [-origin_y / direction_y]
39
+ end
40
+
41
+ # @return [Ra::Tuple]
42
+ def l_normal(*)
43
+ Vector[
44
+ 0,
45
+ 1,
46
+ 0,
47
+ Ra::Tuple::VECTOR
48
+ ]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Shape
5
+ # A sphere at origin <0,0,0> with a radius 1. A sphere surface is defined:
6
+ #
7
+ # x² + y² + z² = radius²
8
+ #
9
+ # A unit radius simplifies further:
10
+ #
11
+ # x² + y² + z² = 1
12
+ #
13
+ # A ray `x` / `y` / `z` values at `t` use the `origin` and `direction`:
14
+ #
15
+ # x = origin.x + direction.x * t
16
+ # y = origin.y + direction.y * t
17
+ # z = origin.z + direction.z * t
18
+ #
19
+ # Substituting `x` / `y` / `z` allows for solving for `t`:
20
+ #
21
+ # 1 = (origin.x + direction.x * t)²
22
+ # + (origin.y + direction.y * t)²
23
+ # + (origin.z + direction.z * t)²
24
+ #
25
+ # Simplifying gives us a quadratic formula with terms defined as:
26
+ #
27
+ # a = direction.x² + direction.y² + direction.z²
28
+ # b = 2 * ((origin.x * direction.x) + (origin.y * direction.y) + (origin.z * direction.z))
29
+ # c = origin.x² + origin.y² + origin.z² - 1
30
+ # discriminant = b² - 4ac
31
+ # t = (-b ± √discriminant) / (2a)
32
+ #
33
+ # A discriminant <0 indicates the ray does not intersect the sphere.
34
+ class Sphere < Base
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
51
+
52
+ # @param ray [Ra::Ray] local
53
+ # @return [Array<Numeric>]
54
+ def t_intersect(ray:)
55
+ origin = ray.origin - Vector[0, 0, 0, Ra::Tuple::POINT]
56
+ direction = ray.direction
57
+
58
+ quadratic(
59
+ a: direction.dot(direction),
60
+ b: 2 * direction.dot(origin),
61
+ c: origin.dot(origin) - 1,
62
+ )
63
+ end
64
+
65
+ # @param point [Vector]
66
+ # @return [Ra::Tuple]
67
+ def l_normal(point:)
68
+ point - Vector[0, 0, 0, Ra::Tuple::VECTOR]
69
+ 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
+ end
89
+ end
90
+ end
data/lib/ra/surface.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A surface contains everything needed to apply lighting.
5
+ class Surface
6
+ attr_accessor :eyev, :normalv, :shape, :point
7
+
8
+ # @param eyev [Vector]
9
+ # @param normalv [Vector]
10
+ # @param shape [Ra::Shape]
11
+ # @param point [Vector]
12
+ def initialize(eyev:, normalv:, shape:, point:)
13
+ @eyev = eyev
14
+ @normalv = normalv.dot(eyev).negative? ? -normalv : +normalv
15
+ @shape = shape
16
+ @point = point
17
+ end
18
+
19
+ # @return [Vector]
20
+ def hpoint
21
+ @hpoint ||= point + (normalv * EPSILON)
22
+ end
23
+ end
24
+ end