ra 0.1.0 → 0.2.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.
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # Lighting encaspulates a [Phong Reflection Model](https://en.wikipedia.org/wiki/phong_reflection_model).
5
+ class Lighting
6
+ attr_accessor :light, :surface, :shadowed
7
+
8
+ # @param light [Ra::Light]
9
+ # @param surface [Ra::Surface]
10
+ # @param shadowed [Boolean]
11
+ def initialize(light:, surface:, shadowed:)
12
+ @surface = surface
13
+ @shadowed = shadowed
14
+ @light = light
15
+ end
16
+
17
+ # @param shadowed [Boolean]
18
+ # @return [Ra::Color]
19
+ def color
20
+ ambient_color + diffuse_color + specular_color
21
+ end
22
+
23
+ private
24
+
25
+ # @param [Ra::Shape]
26
+ def shape
27
+ surface.shape
28
+ end
29
+
30
+ # @return [Vector]
31
+ def point
32
+ surface.point
33
+ end
34
+
35
+ # @return [Vector]
36
+ def normalv
37
+ surface.normalv
38
+ end
39
+
40
+ # @return [Vector]
41
+ def eyev
42
+ surface.eyev
43
+ end
44
+
45
+ # @return [Ra::Material]
46
+ def material
47
+ shape.material
48
+ end
49
+
50
+ # @return [Ra::Vector]
51
+ def lightv
52
+ @lightv ||= (light.position - point).normalize
53
+ end
54
+
55
+ # @return [Ra::Vector]
56
+ def reflectv
57
+ @reflectv ||= -(lightv - (normalv * 2 * lightv.dot(normalv)))
58
+ end
59
+
60
+ # @return [Ra::Vector]
61
+ def light_dot_normal
62
+ @light_dot_normal ||= lightv.dot(normalv)
63
+ end
64
+
65
+ # @return [Ra::Vector]
66
+ def reflect_dot_eye
67
+ @reflect_dot_eye ||= reflectv.dot(eyev)
68
+ end
69
+
70
+ # @return [Ra::Color]
71
+ def ambient_color
72
+ @ambient_color ||= effective_color * material.ambient
73
+ end
74
+
75
+ # @return [Ra::Color, nil]
76
+ def diffuse_color
77
+ return if shadowed
78
+ return if light_dot_normal.negative?
79
+
80
+ @diffuse_color ||= effective_color * material.diffuse * light_dot_normal
81
+ end
82
+
83
+ # @return [Ra::Color, nil]
84
+ def specular_color
85
+ return if shadowed
86
+ return if light_dot_normal.negative?
87
+ return unless reflect_dot_eye.positive?
88
+
89
+ @specular_color ||= light.intensity * material.specular * (reflect_dot_eye**material.shininess)
90
+ end
91
+
92
+ # @return [Ra::Color]
93
+ def effective_color
94
+ @effective_color ||= shape.color(point:) * light.intensity
95
+ end
96
+ end
97
+ end
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.2, diffuse: 0.6, specular: 0.6, shininess: 200)
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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # An abstract pattern. Any concrete subclass of pattern must implement the
6
+ # method `local_color`.
7
+ 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]
16
+ # @return [Ra::Color]
17
+ 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'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A checkers pattern that alternates colors using:
6
+ #
7
+ # colors[⌊√(point.x² + point.z²)⌋]
8
+ class Checkers < Base
9
+ attr_accessor :colors
10
+
11
+ # @param colors [Array<Ra::Color>]
12
+ # @param transform [Ra::Matrix]
13
+ def initialize(colors:, transform: Transform::IDENTITY)
14
+ super(transform:)
15
+ @colors = colors
16
+ end
17
+
18
+ protected
19
+
20
+ # @param local_point [Vector]
21
+ # @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
27
+
28
+ colors[index % colors.count]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
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) * (point.x - point.x.floor)
8
+ class Gradient < Base
9
+ # @param color_a [Ra::Color]
10
+ # @param color_b [Ra::Color]
11
+ # @param transform [Ra::Matrix]
12
+ def initialize(color_a:, color_b:, transform: Transform::IDENTITY)
13
+ super(transform:)
14
+ @color_a = color_a
15
+ @color_b = color_b
16
+ end
17
+
18
+ protected
19
+
20
+ # @param local_point [Vector]
21
+ # @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)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ module Pattern
5
+ # A rings pattern that alternates colors using:
6
+ #
7
+ # colors[⌊√(point.x² + point.z²)⌋]
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:)
15
+ @colors = colors
16
+ end
17
+
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
26
+
27
+ colors[index % colors.count]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
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
+ attr_accessor :colors
10
+
11
+ # @param colors [Array<Ra::Color>]
12
+ # @param transform [Ra::Matrix]
13
+ def initialize(colors:, transform: Transform::IDENTITY)
14
+ super(transform:)
15
+ @colors = colors
16
+ end
17
+
18
+ protected
19
+
20
+ # @param local_point [Vector]
21
+ # @return [Ra::Color]
22
+ def local_color(local_point:)
23
+ x = local_point[0]
24
+ index = x.floor
25
+
26
+ colors[index % colors.count]
27
+ end
28
+ end
29
+ end
30
+ 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,54 @@
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, :transform
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]
26
+ # @return [Vector]
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]
34
+ # @return [Color]
35
+ def color(point:)
36
+ @material.color(point: transform.inverse * point)
37
+ end
38
+
39
+ private
40
+
41
+ # @param ray [Ra::Ray] local
42
+ # @return [Array<Intersection>]
43
+ def t_intersect(ray:)
44
+ raise NotImplementedError, '#t_intersect must be implemented by a concrete subclass'
45
+ end
46
+
47
+ # @param point [Vector] local
48
+ # @return [Vector]
49
+ def l_normal(point:)
50
+ raise NotImplementedError, '#l_normal must be implemented by a concrete subclass'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,89 @@
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
+ protected
25
+
26
+ # @param ray [Ra::Ray] local
27
+ # @return [Array<Numeric>]
28
+ def t_intersect(ray:)
29
+ t_min = t_min(ray:)
30
+ t_max = t_max(ray:)
31
+
32
+ return [] if !t_min || !t_max || t_min > t_max
33
+
34
+ [
35
+ t_min,
36
+ t_max,
37
+ ]
38
+ end
39
+
40
+ # @param point [Vector]
41
+ # @return [Ra::Tuple]
42
+ def l_normal(point:)
43
+ x = point[0].abs
44
+ y = point[1].abs
45
+ z = point[2].abs
46
+
47
+ Vector[
48
+ (is_x = x > y && x > z) ? 1 : 0,
49
+ (is_y = y > x && y > z) ? 1 : 0,
50
+ is_x || is_y ? 0 : 1,
51
+ Ra::Tuple::VECTOR
52
+ ]
53
+ end
54
+
55
+ private
56
+
57
+ # @param ray [Ra::Ray]
58
+ # @return [Integer]
59
+ def t_min(ray:)
60
+ (0..2).map { |i| t_min_max(ray.origin[i], ray.direction[i]).min }.max
61
+ end
62
+
63
+ # @param ray [Ra::Ray]
64
+ # @return [Integer]
65
+ def t_max(ray:)
66
+ (0..2).map { |i| t_min_max(ray.origin[i], ray.direction[i]).max }.min
67
+ end
68
+
69
+ # @param origin [Numeric]
70
+ # @param direction [Numeric]
71
+ # @param value [Numeric]
72
+ # @return [Array<Numeric,Numeric>]
73
+ def t_min_max(origin, direction)
74
+ t_min_numerator = -1 - origin
75
+ t_max_numerator = +1 - origin
76
+
77
+ if direction.abs < EPSILON
78
+ t_min = t_min_numerator * Float::INFINITY
79
+ t_max = t_max_numerator * Float::INFINITY
80
+ else
81
+ t_min = t_min_numerator / direction
82
+ t_max = t_max_numerator / direction
83
+ end
84
+
85
+ t_min < t_max ? [t_min, t_max] : [t_max, t_min]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,45 @@
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
+ protected
22
+
23
+ # @param ray [Ra::Ray] local
24
+ # @return [Array<Numeric>]
25
+ def t_intersect(ray:)
26
+ origin_y = ray.origin[1]
27
+ direction_y = ray.direction[1]
28
+
29
+ return [] if direction_y.abs < EPSILON
30
+
31
+ [-origin_y / direction_y]
32
+ end
33
+
34
+ # @return [Ra::Tuple]
35
+ def l_normal(*)
36
+ Vector[
37
+ 0,
38
+ 1,
39
+ 0,
40
+ Ra::Tuple::VECTOR
41
+ ]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
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
+ protected
36
+
37
+ # @param ray [Ra::Ray] local
38
+ # @return [Array<Numeric>]
39
+ def t_intersect(ray:)
40
+ origin = ray.origin - Vector[0, 0, 0, Ra::Tuple::POINT]
41
+ direction = ray.direction
42
+
43
+ quadratic(
44
+ a: direction.dot(direction),
45
+ b: 2 * direction.dot(origin),
46
+ c: origin.dot(origin) - 1,
47
+ )
48
+ end
49
+
50
+ # @param point [Vector]
51
+ # @return [Ra::Tuple]
52
+ def l_normal(point:)
53
+ point - Vector[0, 0, 0, Ra::Tuple::VECTOR]
54
+ end
55
+
56
+ private
57
+
58
+ # (-b ± √(b² - 4ac)) / (2a)
59
+ #
60
+ # @param a [Numeric]
61
+ # @param b [Numeric]
62
+ # @param c [Numeric]
63
+ # @return [Array<Numeric>]
64
+ def quadratic(a:, b:, c:)
65
+ discriminant = (b**2) - (4 * a * c)
66
+ return [] if discriminant.negative?
67
+
68
+ [
69
+ (-b - Math.sqrt(discriminant)) / (2 * a),
70
+ (-b + Math.sqrt(discriminant)) / (2 * a),
71
+ ]
72
+ end
73
+ end
74
+ end
75
+ 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
+ point + (normalv * EPSILON)
22
+ end
23
+ end
24
+ end