ra 0.1.0 → 0.3.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 +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
|