ra 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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