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.
- checksums.yaml +4 -4
- data/README.md +8 -22
- data/exe/ra +138 -0
- data/lib/ra/camera.rb +92 -0
- data/lib/ra/canvas.rb +78 -0
- data/lib/ra/color.rb +149 -0
- data/lib/ra/engine.rb +40 -0
- data/lib/ra/intersection.rb +45 -0
- data/lib/ra/light.rb +21 -0
- data/lib/ra/lighting.rb +97 -0
- data/lib/ra/logger.rb +19 -0
- data/lib/ra/material.rb +37 -0
- data/lib/ra/pattern/base.rb +31 -0
- data/lib/ra/pattern/checkers.rb +32 -0
- data/lib/ra/pattern/gradient.rb +29 -0
- data/lib/ra/pattern/rings.rb +31 -0
- data/lib/ra/pattern/stripes.rb +30 -0
- data/lib/ra/ray.rb +53 -0
- data/lib/ra/shape/base.rb +54 -0
- data/lib/ra/shape/cube.rb +89 -0
- data/lib/ra/shape/plane.rb +45 -0
- data/lib/ra/shape/sphere.rb +75 -0
- data/lib/ra/surface.rb +24 -0
- data/lib/ra/transform.rb +141 -0
- data/lib/ra/tuple.rb +26 -0
- data/lib/ra/version.rb +1 -1
- data/lib/ra/world.rb +48 -0
- data/lib/ra.rb +12 -3
- metadata +77 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/Rakefile +0 -12
- data/sig/ra.rbs +0 -4
data/lib/ra/lighting.rb
ADDED
@@ -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
|
data/lib/ra/material.rb
ADDED
@@ -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
|