ra 0.2.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 +3 -2
- data/exe/ra +36 -44
- data/lib/ra/camera.rb +1 -1
- data/lib/ra/canvas.rb +2 -9
- data/lib/ra/color.rb +19 -10
- data/lib/ra/lighting.rb +1 -2
- data/lib/ra/material.rb +1 -1
- data/lib/ra/pattern/base.rb +3 -20
- data/lib/ra/pattern/checkers.rb +19 -14
- data/lib/ra/pattern/gradient.rb +10 -11
- data/lib/ra/pattern/rings.rb +11 -16
- data/lib/ra/pattern/stripes.rb +9 -12
- data/lib/ra/pattern/texture.rb +52 -0
- data/lib/ra/shape/base.rb +12 -8
- data/lib/ra/shape/cube.rb +68 -2
- data/lib/ra/shape/plane.rb +8 -1
- data/lib/ra/shape/sphere.rb +16 -1
- data/lib/ra/surface.rb +1 -1
- data/lib/ra/tuple.rb +2 -2
- data/lib/ra/version.rb +1 -1
- data/lib/ra/world.rb +13 -9
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b56b6d752ce23d6484526702a0702d865bfad49f0209a40a8e597adf7a3c1b13
|
4
|
+
data.tar.gz: e45c8ddc28b1723f9e2a114386a2a0927026e9cf2cb54a38f438f3583d84710c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b2723e84ab6a64a4036615d298e733714838a754aecd59e0146eef97bd7b3c01374a3a9b4e145e9bb3ac437fe6499b41d819bf3fb79be92494bd2a2629c6f39
|
7
|
+
data.tar.gz: 1f74e78ddc9fbd2c61e231601d08519a8279279418ce8df0e8d6528a183412e09bbbfd138cbb3cd15fcb96979cac30b312520b3694b2c1232d394a1598dcfa57
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Ra
|
2
2
|
|
3
|
-
Named for [
|
3
|
+
Named for [Ra](https://en.wikipedia.org/wiki/Ra) - arguably the original ray tracer.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -11,7 +11,8 @@ gem install ra
|
|
11
11
|
## Usage
|
12
12
|
|
13
13
|
```sh
|
14
|
-
ra -w 2560 -h 2048
|
14
|
+
ra -w 2560 -h 2048 > sample.ppm
|
15
|
+
convert -quality 80 sample.ppm sample.avif
|
15
16
|
```
|
16
17
|
|
17
18
|

|
data/exe/ra
CHANGED
@@ -23,9 +23,16 @@ config = Slop.parse(ARGV) do |options|
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'textures/earth.avif'))
|
27
|
+
|
28
|
+
light_l = Ra::Light.new(
|
27
29
|
position: Vector[+5, +7, -9, Ra::Tuple::POINT],
|
28
|
-
intensity: Ra::Color.
|
30
|
+
intensity: Ra::Color.uniform(0.4),
|
31
|
+
)
|
32
|
+
|
33
|
+
light_r = Ra::Light.new(
|
34
|
+
position: Vector[-5, +7, -9, Ra::Tuple::POINT],
|
35
|
+
intensity: Ra::Color.uniform(0.8),
|
29
36
|
)
|
30
37
|
|
31
38
|
camera = Ra::Camera.new(
|
@@ -39,33 +46,22 @@ camera = Ra::Camera.new(
|
|
39
46
|
),
|
40
47
|
)
|
41
48
|
|
42
|
-
floor_material = Ra::Material.new(base: Ra::Pattern::Checkers.new(
|
43
|
-
colors: [
|
44
|
-
Ra::Color.hex('#e2e8f0'),
|
45
|
-
Ra::Color.hex('#94a3b8'),
|
46
|
-
],
|
47
|
-
transform: Ra::Transform
|
48
|
-
.scale(0.4, 0.4, 0.4)
|
49
|
-
.translate(0, +0.2, 0),
|
50
|
-
))
|
51
|
-
|
52
|
-
wall_material = Ra::Material.new(base: Ra::Pattern::Stripes.new(
|
53
|
-
colors: [
|
54
|
-
Ra::Color.hex('#94a3b8'),
|
55
|
-
Ra::Color.hex('#475569'),
|
56
|
-
],
|
57
|
-
transform: Ra::Transform
|
58
|
-
.rotate_x(Math::PI / 4)
|
59
|
-
.rotate_y(Math::PI / 4)
|
60
|
-
.scale(0.2, 0.2, 0.2),
|
61
|
-
))
|
62
|
-
|
63
49
|
floor = Ra::Shape::Plane.new(
|
64
|
-
material:
|
50
|
+
material: Ra::Material.new(base: Ra::Pattern::Checkers.new(
|
51
|
+
colors: [
|
52
|
+
Ra::Color.hex('#e2e8f0'),
|
53
|
+
Ra::Color.hex('#94a3b8'),
|
54
|
+
],
|
55
|
+
)),
|
65
56
|
)
|
66
57
|
|
67
58
|
wall_l = Ra::Shape::Plane.new(
|
68
|
-
material:
|
59
|
+
material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
|
60
|
+
colors: [
|
61
|
+
Ra::Color.hex('#94a3b8'),
|
62
|
+
Ra::Color.hex('#475569'),
|
63
|
+
],
|
64
|
+
)),
|
69
65
|
transform: Ra::Transform
|
70
66
|
.translate(0, 0, +5.0)
|
71
67
|
.rotate_y(-Math::PI / 4)
|
@@ -73,7 +69,12 @@ wall_l = Ra::Shape::Plane.new(
|
|
73
69
|
)
|
74
70
|
|
75
71
|
wall_r = Ra::Shape::Plane.new(
|
76
|
-
material:
|
72
|
+
material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
|
73
|
+
colors: [
|
74
|
+
Ra::Color.hex('#94a3b8'),
|
75
|
+
Ra::Color.hex('#475569'),
|
76
|
+
],
|
77
|
+
)),
|
77
78
|
transform: Ra::Transform
|
78
79
|
.translate(0, 0, +5.0)
|
79
80
|
.rotate_y(Math::PI / 4)
|
@@ -81,18 +82,10 @@ wall_r = Ra::Shape::Plane.new(
|
|
81
82
|
)
|
82
83
|
|
83
84
|
sphere = Ra::Shape::Sphere.new(
|
84
|
-
material: Ra::Material.new(base:
|
85
|
-
colors: [
|
86
|
-
Ra::Color.hex('#f87171'),
|
87
|
-
Ra::Color.hex('#dc2626'),
|
88
|
-
],
|
89
|
-
transform: Ra::Transform
|
90
|
-
.rotate_x(Math::PI / 4)
|
91
|
-
.rotate_y(Math::PI / 4)
|
92
|
-
.scale(0.2, 0.2, 0.2),
|
93
|
-
)),
|
85
|
+
material: Ra::Material.new(base: earth),
|
94
86
|
transform: Ra::Transform
|
95
87
|
.translate(0, +0.5, -2.0)
|
88
|
+
.rotate_y(Math::PI / 2)
|
96
89
|
.scale(0.5, 0.5, 0.5),
|
97
90
|
)
|
98
91
|
|
@@ -100,9 +93,6 @@ cube_l = Ra::Shape::Cube.new(
|
|
100
93
|
material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
|
101
94
|
color_a: Ra::Color.hex('#f43f5e'),
|
102
95
|
color_b: Ra::Color.hex('#8b5cf6'),
|
103
|
-
transform: Ra::Transform
|
104
|
-
.translate(1.0, 1.0, 1.0)
|
105
|
-
.scale(3.0, 3.0, 3.0),
|
106
96
|
)),
|
107
97
|
transform: Ra::Transform
|
108
98
|
.translate(+1.0, +0.3, -1.5)
|
@@ -113,15 +103,17 @@ cube_r = Ra::Shape::Cube.new(
|
|
113
103
|
material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
|
114
104
|
color_a: Ra::Color.hex('#84cc16'),
|
115
105
|
color_b: Ra::Color.hex('#f97316'),
|
116
|
-
transform: Ra::Transform
|
117
|
-
.translate(1.0, 1.0, 1.0)
|
118
|
-
.scale(3.0, 3.0, 3.0),
|
119
106
|
)),
|
120
107
|
transform: Ra::Transform
|
121
|
-
.translate(-1.0,
|
108
|
+
.translate(-1.0, +0.3, -1.5)
|
122
109
|
.scale(0.3, 0.3, 0.3),
|
123
110
|
)
|
124
111
|
|
112
|
+
lights = [
|
113
|
+
light_l,
|
114
|
+
light_r,
|
115
|
+
]
|
116
|
+
|
125
117
|
shapes = [
|
126
118
|
floor,
|
127
119
|
wall_l,
|
@@ -131,7 +123,7 @@ shapes = [
|
|
131
123
|
cube_r,
|
132
124
|
].freeze
|
133
125
|
|
134
|
-
world = Ra::World.new(
|
126
|
+
world = Ra::World.new(lights:, shapes:)
|
135
127
|
engine = Ra::Engine.new(camera:, world:)
|
136
128
|
canvas = engine.render
|
137
129
|
|
data/lib/ra/camera.rb
CHANGED
data/lib/ra/canvas.rb
CHANGED
@@ -16,19 +16,12 @@ module Ra
|
|
16
16
|
class Canvas
|
17
17
|
attr_accessor :w, :h, :precision
|
18
18
|
|
19
|
-
DEFAULT_COLOR = Color.black
|
20
|
-
private_constant :DEFAULT_COLOR
|
21
|
-
|
22
|
-
DEFAULT_PRECISION = 255
|
23
|
-
private_constant :DEFAULT_PRECISION
|
24
|
-
|
25
19
|
PPM_VERSION = 'P3'
|
26
|
-
private_constant :PPM_VERSION
|
27
20
|
|
28
21
|
# @param w [Integer]
|
29
22
|
# @param h [Integer]
|
30
23
|
# @param precision [Integer]
|
31
|
-
def initialize(w:, h:, precision:
|
24
|
+
def initialize(w:, h:, precision: Color::PRECISION)
|
32
25
|
@w = w
|
33
26
|
@h = h
|
34
27
|
@precision = precision
|
@@ -43,7 +36,7 @@ module Ra
|
|
43
36
|
raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
|
44
37
|
raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
|
45
38
|
|
46
|
-
@pixels[x][y] ||
|
39
|
+
@pixels[x][y] || Color.black
|
47
40
|
end
|
48
41
|
|
49
42
|
# @param x [Integer]
|
data/lib/ra/color.rb
CHANGED
@@ -19,8 +19,17 @@ module Ra
|
|
19
19
|
class Color
|
20
20
|
attr_accessor :r, :g, :b
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
PRECISION = 255
|
23
|
+
|
24
|
+
# @param value [Array<Numeric,Numeric,Numeric>]
|
25
|
+
# @return [Ra::Color]
|
26
|
+
def self.[](value)
|
27
|
+
new(
|
28
|
+
r: value[0],
|
29
|
+
g: value[1],
|
30
|
+
b: value[2],
|
31
|
+
)
|
32
|
+
end
|
24
33
|
|
25
34
|
# @param value [String] e.g. "#336699"
|
26
35
|
# @return [Ra::Color]
|
@@ -28,9 +37,9 @@ module Ra
|
|
28
37
|
r, g, b = value.match(/^#(..)(..)(..)$/).captures.map(&:hex)
|
29
38
|
|
30
39
|
new(
|
31
|
-
r: Float(r) /
|
32
|
-
g: Float(g) /
|
33
|
-
b: Float(b) /
|
40
|
+
r: Float(r) / PRECISION,
|
41
|
+
g: Float(g) / PRECISION,
|
42
|
+
b: Float(b) / PRECISION,
|
34
43
|
)
|
35
44
|
end
|
36
45
|
|
@@ -61,7 +70,7 @@ module Ra
|
|
61
70
|
|
62
71
|
# @param precision [Integer]
|
63
72
|
# @return [Integer]
|
64
|
-
def ppm(precision:
|
73
|
+
def ppm(precision: PRECISION)
|
65
74
|
"#{r_val(precision:)} #{g_val(precision:)} #{b_val(precision:)}"
|
66
75
|
end
|
67
76
|
|
@@ -123,17 +132,17 @@ module Ra
|
|
123
132
|
protected
|
124
133
|
|
125
134
|
# @return [Integer]
|
126
|
-
def r_val(precision:
|
135
|
+
def r_val(precision: PRECISION)
|
127
136
|
val(value: r, precision:)
|
128
137
|
end
|
129
138
|
|
130
139
|
# @return [Integer]
|
131
|
-
def g_val(precision:
|
140
|
+
def g_val(precision: PRECISION)
|
132
141
|
val(value: g, precision:)
|
133
142
|
end
|
134
143
|
|
135
144
|
# @return [Integer]
|
136
|
-
def b_val(precision:
|
145
|
+
def b_val(precision: PRECISION)
|
137
146
|
val(value: b, precision:)
|
138
147
|
end
|
139
148
|
|
@@ -142,7 +151,7 @@ module Ra
|
|
142
151
|
# @param value [Numeric]
|
143
152
|
# @param precision [Integer]
|
144
153
|
# @return [Integer]
|
145
|
-
def val(value:, precision:
|
154
|
+
def val(value:, precision: PRECISION)
|
146
155
|
(value * precision).clamp(0, precision).round
|
147
156
|
end
|
148
157
|
end
|
data/lib/ra/lighting.rb
CHANGED
@@ -14,7 +14,6 @@ module Ra
|
|
14
14
|
@light = light
|
15
15
|
end
|
16
16
|
|
17
|
-
# @param shadowed [Boolean]
|
18
17
|
# @return [Ra::Color]
|
19
18
|
def color
|
20
19
|
ambient_color + diffuse_color + specular_color
|
@@ -22,7 +21,7 @@ module Ra
|
|
22
21
|
|
23
22
|
private
|
24
23
|
|
25
|
-
# @
|
24
|
+
# @return [Ra::Shape]
|
26
25
|
def shape
|
27
26
|
surface.shape
|
28
27
|
end
|
data/lib/ra/material.rb
CHANGED
@@ -18,7 +18,7 @@ module Ra
|
|
18
18
|
# @param diffuse [Float] between 0.0 and 1.0
|
19
19
|
# @param specular [Float] between 0.0 and 1.0
|
20
20
|
# @param shininess [Numeric]
|
21
|
-
def initialize(base:, ambient: 0.
|
21
|
+
def initialize(base:, ambient: 0.0, diffuse: 0.8, specular: 0.2, shininess: 80)
|
22
22
|
@base = base
|
23
23
|
@ambient = ambient
|
24
24
|
@diffuse = diffuse
|
data/lib/ra/pattern/base.rb
CHANGED
@@ -2,29 +2,12 @@
|
|
2
2
|
|
3
3
|
module Ra
|
4
4
|
module Pattern
|
5
|
-
# An abstract pattern. Any concrete subclass of pattern must implement the
|
6
|
-
# method `local_color`.
|
5
|
+
# An abstract pattern. Any concrete subclass of pattern must implement the method `color`.
|
7
6
|
class Base
|
8
|
-
|
9
|
-
|
10
|
-
# @param transform [Ra::Matrix]
|
11
|
-
def initialize(transform: Transform::IDENTITY)
|
12
|
-
@transform = transform
|
13
|
-
end
|
14
|
-
|
15
|
-
# @param point [Vector]
|
7
|
+
# @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
16
8
|
# @return [Ra::Color]
|
17
9
|
def color(point:)
|
18
|
-
|
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'
|
10
|
+
raise NotImplementedError, '#color must be implemented by a concrete subclass'
|
28
11
|
end
|
29
12
|
end
|
30
13
|
end
|
data/lib/ra/pattern/checkers.rb
CHANGED
@@ -4,28 +4,33 @@ module Ra
|
|
4
4
|
module Pattern
|
5
5
|
# A checkers pattern that alternates colors using:
|
6
6
|
#
|
7
|
-
# colors[
|
7
|
+
# colors[⌊u * rows⌋ + ⌊v * cols)⌋ % colors.count]
|
8
8
|
class Checkers < Base
|
9
|
-
|
9
|
+
DEFAULT_ROWS = 2
|
10
|
+
DEFAULT_COLS = 2
|
11
|
+
DEFAULT_COLORS = [
|
12
|
+
Color.black,
|
13
|
+
Color.white,
|
14
|
+
].freeze
|
10
15
|
|
16
|
+
# @param rows [Integer]
|
17
|
+
# @param cols [Integer]
|
11
18
|
# @param colors [Array<Ra::Color>]
|
12
|
-
|
13
|
-
|
14
|
-
|
19
|
+
def initialize(cols: DEFAULT_COLS, rows: DEFAULT_ROWS, colors: DEFAULT_COLORS)
|
20
|
+
super()
|
21
|
+
@rows = rows
|
22
|
+
@cols = cols
|
15
23
|
@colors = colors
|
16
24
|
end
|
17
25
|
|
18
|
-
|
19
|
-
|
20
|
-
# @param local_point [Vector]
|
26
|
+
# @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
21
27
|
# @return [Ra::Color]
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
index = x.floor + y.floor + z.floor
|
28
|
+
def color(point:)
|
29
|
+
u = point[0]
|
30
|
+
v = point[1]
|
31
|
+
index = (u * @rows).floor + (v * @cols).floor
|
27
32
|
|
28
|
-
colors[index % colors.count]
|
33
|
+
@colors[index % @colors.count]
|
29
34
|
end
|
30
35
|
end
|
31
36
|
end
|
data/lib/ra/pattern/gradient.rb
CHANGED
@@ -4,25 +4,24 @@ module Ra
|
|
4
4
|
module Pattern
|
5
5
|
# A graident pattern from `color_a` to `color_b` using:
|
6
6
|
#
|
7
|
-
# color_b + (color_b - color_a) * (
|
7
|
+
# color_b + (color_b - color_a) * (u + v) / 2
|
8
8
|
class Gradient < Base
|
9
9
|
# @param color_a [Ra::Color]
|
10
10
|
# @param color_b [Ra::Color]
|
11
|
-
|
12
|
-
|
13
|
-
super(transform:)
|
11
|
+
def initialize(color_a:, color_b:)
|
12
|
+
super()
|
14
13
|
@color_a = color_a
|
15
14
|
@color_b = color_b
|
16
15
|
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
# @param local_point [Vector]
|
17
|
+
# @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
21
18
|
# @return [Ra::Color]
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
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)
|
26
25
|
end
|
27
26
|
end
|
28
27
|
end
|
data/lib/ra/pattern/rings.rb
CHANGED
@@ -4,27 +4,22 @@ module Ra
|
|
4
4
|
module Pattern
|
5
5
|
# A rings pattern that alternates colors using:
|
6
6
|
#
|
7
|
-
# colors[⌊√(
|
7
|
+
# colors[⌊√(u² + v²)⌋]
|
8
8
|
class Rings < Base
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# @param transform [Rays::Matrix]
|
13
|
-
def initialize(colors:, transform: DEFAULT_TRANSFORM)
|
14
|
-
super(transform:)
|
9
|
+
# @param colors [Array<Ra::Color>]
|
10
|
+
def initialize(colors:)
|
11
|
+
super()
|
15
12
|
@colors = colors
|
16
13
|
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
z = local_point[2]
|
25
|
-
index = Math.sqrt((x**2) + (z**2)).floor
|
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
|
26
21
|
|
27
|
-
colors[index % colors.count]
|
22
|
+
@colors[index % @colors.count]
|
28
23
|
end
|
29
24
|
end
|
30
25
|
end
|
data/lib/ra/pattern/stripes.rb
CHANGED
@@ -6,24 +6,21 @@ module Ra
|
|
6
6
|
#
|
7
7
|
# colors[⌊point.x⌋]
|
8
8
|
class Stripes < Base
|
9
|
-
attr_accessor :colors
|
10
|
-
|
11
9
|
# @param colors [Array<Ra::Color>]
|
12
|
-
|
13
|
-
|
14
|
-
super(transform:)
|
10
|
+
def initialize(colors:)
|
11
|
+
super()
|
15
12
|
@colors = colors
|
16
13
|
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
# @param local_point [Vector]
|
15
|
+
# @param point [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
21
16
|
# @return [Ra::Color]
|
22
|
-
def
|
23
|
-
|
24
|
-
|
17
|
+
def color(point:)
|
18
|
+
count = @colors.count
|
19
|
+
u = point[0]
|
20
|
+
v = point[1]
|
21
|
+
value = (u + v) * (2 * count)
|
25
22
|
|
26
|
-
colors[
|
23
|
+
@colors[value.floor % count]
|
27
24
|
end
|
28
25
|
end
|
29
26
|
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/shape/base.rb
CHANGED
@@ -6,7 +6,7 @@ module Ra
|
|
6
6
|
# methods `l_normal` and `t_intersect`. Both methods use a point / ray
|
7
7
|
# with a local transform applied.
|
8
8
|
class Base
|
9
|
-
attr_accessor :material
|
9
|
+
attr_accessor :material
|
10
10
|
|
11
11
|
# @param material [Ra::Material]
|
12
12
|
# @param transform [Ra::Matrix]
|
@@ -18,25 +18,29 @@ module Ra
|
|
18
18
|
# @param ray [Ra::Ray]
|
19
19
|
# @return [Array<Ra::Intersection>]
|
20
20
|
def intersect(ray:)
|
21
|
-
t_intersect(ray: ray.transform(transform.inverse))
|
21
|
+
t_intersect(ray: ray.transform(@transform.inverse))
|
22
22
|
.map { |t| Ra::Intersection.new(ray:, shape: self, t:) }
|
23
23
|
end
|
24
24
|
|
25
|
-
# @param point [Vector]
|
26
|
-
# @return [Vector]
|
25
|
+
# @param point [Vector] <x, y, z, Tuple::POINT>
|
26
|
+
# @return [Vector] <x, y, z, Tuple::POINT>
|
27
27
|
def normal(point:)
|
28
|
-
normal = transform.inverse.transpose * l_normal(point: transform.inverse * point)
|
28
|
+
normal = @transform.inverse.transpose * l_normal(point: @transform.inverse * point)
|
29
29
|
|
30
30
|
Vector[normal[0], normal[1], normal[2], Ra::Tuple::VECTOR].normalize
|
31
31
|
end
|
32
32
|
|
33
|
-
# @param point [Vector]
|
33
|
+
# @param point [Vector] <x, y, z, Tuple::POINT>
|
34
34
|
# @return [Color]
|
35
35
|
def color(point:)
|
36
|
-
@material.color(point: transform.inverse * point)
|
36
|
+
@material.color(point: uv_point(point: @transform.inverse * point))
|
37
37
|
end
|
38
38
|
|
39
|
-
|
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
|
40
44
|
|
41
45
|
# @param ray [Ra::Ray] local
|
42
46
|
# @return [Array<Intersection>]
|
data/lib/ra/shape/cube.rb
CHANGED
@@ -21,7 +21,20 @@ module Ra
|
|
21
21
|
#
|
22
22
|
# Thus 6 planes can be checked for intersect.
|
23
23
|
class Cube < Base
|
24
|
-
|
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
|
25
38
|
|
26
39
|
# @param ray [Ra::Ray] local
|
27
40
|
# @return [Array<Numeric>]
|
@@ -68,7 +81,6 @@ module Ra
|
|
68
81
|
|
69
82
|
# @param origin [Numeric]
|
70
83
|
# @param direction [Numeric]
|
71
|
-
# @param value [Numeric]
|
72
84
|
# @return [Array<Numeric,Numeric>]
|
73
85
|
def t_min_max(origin, direction)
|
74
86
|
t_min_numerator = -1 - origin
|
@@ -84,6 +96,60 @@ module Ra
|
|
84
96
|
|
85
97
|
t_min < t_max ? [t_min, t_max] : [t_max, t_min]
|
86
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
|
87
153
|
end
|
88
154
|
end
|
89
155
|
end
|
data/lib/ra/shape/plane.rb
CHANGED
@@ -18,7 +18,14 @@ module Ra
|
|
18
18
|
#
|
19
19
|
# A direction.y < EPISLON indicates the ray does not intersect the plane.
|
20
20
|
class Plane < Base
|
21
|
-
|
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
|
22
29
|
|
23
30
|
# @param ray [Ra::Ray] local
|
24
31
|
# @return [Array<Numeric>]
|
data/lib/ra/shape/sphere.rb
CHANGED
@@ -32,7 +32,22 @@ module Ra
|
|
32
32
|
#
|
33
33
|
# A discriminant <0 indicates the ray does not intersect the sphere.
|
34
34
|
class Sphere < Base
|
35
|
-
|
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
|
36
51
|
|
37
52
|
# @param ray [Ra::Ray] local
|
38
53
|
# @return [Array<Numeric>]
|
data/lib/ra/surface.rb
CHANGED
data/lib/ra/tuple.rb
CHANGED
@@ -9,8 +9,8 @@ module Ra
|
|
9
9
|
POINT = 1
|
10
10
|
VECTOR = 0
|
11
11
|
|
12
|
-
# @
|
13
|
-
# @
|
12
|
+
# @param source [Vector]
|
13
|
+
# @param target [Vector]
|
14
14
|
# @return [Vector]
|
15
15
|
def self.cross(source, target)
|
16
16
|
cross = Vector[source[0], source[1], source[2]].cross(Vector[target[0], target[1], target[2]])
|
data/lib/ra/version.rb
CHANGED
data/lib/ra/world.rb
CHANGED
@@ -3,12 +3,10 @@
|
|
3
3
|
module Ra
|
4
4
|
# A world is composed of objects (lights / cameras) and handles coloring of rays.
|
5
5
|
class World
|
6
|
-
|
7
|
-
|
8
|
-
# @param light [Ra::Light]
|
6
|
+
# @param lights [Ra::Light]
|
9
7
|
# @param shapes [Array<Ra::Shape>]
|
10
|
-
def initialize(
|
11
|
-
@
|
8
|
+
def initialize(lights:, shapes:)
|
9
|
+
@lights = lights
|
12
10
|
@shapes = shapes
|
13
11
|
end
|
14
12
|
|
@@ -30,14 +28,20 @@ module Ra
|
|
30
28
|
# @return [Ra::Color]
|
31
29
|
def color(intersection:)
|
32
30
|
surface = intersection.surface
|
33
|
-
|
31
|
+
point = surface.hpoint
|
32
|
+
|
33
|
+
colors = @lights.map do |light|
|
34
|
+
shadowed = shadowed?(point:, light:)
|
35
|
+
lighting = Lighting.new(surface:, light:, shadowed:)
|
36
|
+
lighting.color
|
37
|
+
end
|
34
38
|
|
35
|
-
|
39
|
+
colors.reduce(&:+)
|
36
40
|
end
|
37
41
|
|
38
42
|
# @param point [Ra::Point]
|
39
|
-
def shadowed?(point:)
|
40
|
-
vector =
|
43
|
+
def shadowed?(light:, point:)
|
44
|
+
vector = light.position - point
|
41
45
|
distance = vector.magnitude
|
42
46
|
direction = vector.normalize
|
43
47
|
ray = Ray.new(origin: point, direction:)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Sylvestre
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: matrix
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mini_magick
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: slop
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -77,6 +91,7 @@ files:
|
|
77
91
|
- lib/ra/pattern/gradient.rb
|
78
92
|
- lib/ra/pattern/rings.rb
|
79
93
|
- lib/ra/pattern/stripes.rb
|
94
|
+
- lib/ra/pattern/texture.rb
|
80
95
|
- lib/ra/ray.rb
|
81
96
|
- lib/ra/shape/base.rb
|
82
97
|
- lib/ra/shape/cube.rb
|