ra 0.1.0 → 0.3.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 +9 -22
- data/exe/ra +130 -0
- data/lib/ra/camera.rb +92 -0
- data/lib/ra/canvas.rb +71 -0
- data/lib/ra/color.rb +158 -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 +96 -0
- data/lib/ra/logger.rb +19 -0
- data/lib/ra/material.rb +37 -0
- data/lib/ra/pattern/base.rb +14 -0
- data/lib/ra/pattern/checkers.rb +37 -0
- data/lib/ra/pattern/gradient.rb +28 -0
- data/lib/ra/pattern/rings.rb +26 -0
- data/lib/ra/pattern/stripes.rb +27 -0
- data/lib/ra/pattern/texture.rb +52 -0
- data/lib/ra/ray.rb +53 -0
- data/lib/ra/shape/base.rb +58 -0
- data/lib/ra/shape/cube.rb +155 -0
- data/lib/ra/shape/plane.rb +52 -0
- data/lib/ra/shape/sphere.rb +90 -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 +52 -0
- data/lib/ra.rb +12 -3
- metadata +92 -13
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/Rakefile +0 -12
- data/sig/ra.rbs +0 -4
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.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
|