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