ra 0.2.0 → 0.4.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 +71 -61
- data/lib/ra/camera.rb +1 -1
- data/lib/ra/canvas.rb +2 -9
- data/lib/ra/color.rb +19 -10
- data/lib/ra/engine.rb +1 -4
- data/lib/ra/intersection.rb +2 -1
- data/lib/ra/lighting.rb +2 -3
- data/lib/ra/material.rb +9 -2
- 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 +4 -3
- data/lib/ra/tuple.rb +9 -2
- data/lib/ra/version.rb +1 -1
- data/lib/ra/world.rb +33 -11
- 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: 96608d9848ea1181d69813cd6a37cfcc5cb0ac04564048ed4b2a9d6f4af27414
|
4
|
+
data.tar.gz: a1bfbd9254cc6c56abab623bdf65d9a3282904c4b43039c3609934a2d8d273b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b63eba61eb50a16fc55388b409b27dde40e092009f920ffe614fb15e29683ac58b129a2bf3848c0944b220ed2b6aca1cfb434a2441d050a601a801fb60c5e0ae
|
7
|
+
data.tar.gz: 004eb7ca2f72a3804f62332f1f909cd2875dbc46928ea6392e22b32a34ae5a3ea55117a6bda5dfd15b84b33d8f1c3d990517b10b1d64ed08497e3fe5b8619750
|
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
@@ -8,8 +8,8 @@ require 'slop'
|
|
8
8
|
config = Slop.parse(ARGV) do |options|
|
9
9
|
options.banner = 'Usage: ra -w 2560 -h 2048 | convert - sample.avif'
|
10
10
|
|
11
|
-
options.integer '-w', '--width', 'width', default:
|
12
|
-
options.integer '-h', '--height', 'height', default:
|
11
|
+
options.integer '-w', '--width', 'width', default: 2560
|
12
|
+
options.integer '-h', '--height', 'height', default: 2048
|
13
13
|
options.integer '-fov', 'degrees', default: 60
|
14
14
|
|
15
15
|
options.on('--help', 'help') do
|
@@ -23,9 +23,16 @@ config = Slop.parse(ARGV) do |options|
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'textures/earth.avif'))
|
27
|
+
|
28
|
+
light_l = Ra::Light.new(
|
29
|
+
position: Vector[+5, +3, -9, Ra::Tuple::POINT],
|
30
|
+
intensity: Ra::Color.uniform(0.5),
|
31
|
+
)
|
32
|
+
|
33
|
+
light_r = Ra::Light.new(
|
34
|
+
position: Vector[-5, +3, -9, Ra::Tuple::POINT],
|
35
|
+
intensity: Ra::Color.uniform(0.5),
|
29
36
|
)
|
30
37
|
|
31
38
|
camera = Ra::Camera.new(
|
@@ -33,95 +40,98 @@ camera = Ra::Camera.new(
|
|
33
40
|
h: config[:h],
|
34
41
|
fov: config[:fov] * Math::PI / 180,
|
35
42
|
transform: Ra::Transform.view(
|
36
|
-
from: Vector[0, +1.5, -
|
37
|
-
to: Vector[0, 0,
|
43
|
+
from: Vector[0, +1.5, -4.0, Ra::Tuple::POINT],
|
44
|
+
to: Vector[0, 0, 0, Ra::Tuple::POINT],
|
38
45
|
up: Vector[0, 1, 0, Ra::Tuple::VECTOR],
|
39
46
|
),
|
40
47
|
)
|
41
48
|
|
42
|
-
|
43
|
-
|
44
|
-
Ra::
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
],
|
49
|
+
floor = Ra::Shape::Plane.new(
|
50
|
+
material: Ra::Material.new(
|
51
|
+
base: Ra::Pattern::Checkers.new(
|
52
|
+
colors: [
|
53
|
+
Ra::Color.hex('#e2e8f0'),
|
54
|
+
Ra::Color.hex('#1e293b'),
|
55
|
+
],
|
56
|
+
),
|
57
|
+
reflective: 0.2,
|
58
|
+
),
|
57
59
|
transform: Ra::Transform
|
58
|
-
.rotate_x(Math::PI / 4)
|
59
60
|
.rotate_y(Math::PI / 4)
|
60
|
-
.scale(0.
|
61
|
-
))
|
62
|
-
|
63
|
-
floor = Ra::Shape::Plane.new(
|
64
|
-
material: floor_material,
|
61
|
+
.scale(0.5, 0.5, 0.5),
|
65
62
|
)
|
66
63
|
|
67
64
|
wall_l = Ra::Shape::Plane.new(
|
68
|
-
material:
|
65
|
+
material: Ra::Material.new(
|
66
|
+
base: Ra::Pattern::Stripes.new(
|
67
|
+
colors: [
|
68
|
+
Ra::Color.hex('#f5f5f4'),
|
69
|
+
Ra::Color.hex('#e7e5e4'),
|
70
|
+
],
|
71
|
+
),
|
72
|
+
),
|
69
73
|
transform: Ra::Transform
|
70
|
-
.translate(0, 0, +
|
74
|
+
.translate(0, 0, +3.0)
|
71
75
|
.rotate_y(-Math::PI / 4)
|
72
76
|
.rotate_x(Math::PI / 2),
|
73
77
|
)
|
74
78
|
|
75
79
|
wall_r = Ra::Shape::Plane.new(
|
76
|
-
material:
|
80
|
+
material: Ra::Material.new(
|
81
|
+
base: Ra::Pattern::Stripes.new(
|
82
|
+
colors: [
|
83
|
+
Ra::Color.hex('#f5f5f4'),
|
84
|
+
Ra::Color.hex('#e7e5e4'),
|
85
|
+
],
|
86
|
+
),
|
87
|
+
),
|
77
88
|
transform: Ra::Transform
|
78
|
-
.translate(0, 0, +
|
89
|
+
.translate(0, 0, +3.0)
|
79
90
|
.rotate_y(Math::PI / 4)
|
80
91
|
.rotate_x(Math::PI / 2),
|
81
92
|
)
|
82
93
|
|
83
94
|
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
|
-
)),
|
95
|
+
material: Ra::Material.new(base: earth),
|
94
96
|
transform: Ra::Transform
|
95
97
|
.translate(0, +0.5, -2.0)
|
98
|
+
.rotate_y(Math::PI / 2)
|
96
99
|
.scale(0.5, 0.5, 0.5),
|
97
100
|
)
|
98
101
|
|
99
102
|
cube_l = Ra::Shape::Cube.new(
|
100
|
-
material: Ra::Material.new(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
)
|
103
|
+
material: Ra::Material.new(
|
104
|
+
base: Ra::Pattern::Gradient.new(
|
105
|
+
color_a: Ra::Color.hex('#f43f5e'),
|
106
|
+
color_b: Ra::Color.hex('#8b5cf6'),
|
107
|
+
),
|
108
|
+
reflective: 0.2,
|
109
|
+
),
|
107
110
|
transform: Ra::Transform
|
108
|
-
.translate(+1.0, +0.3, -1.
|
109
|
-
.scale(0.3, 0.3, 0.3)
|
111
|
+
.translate(+1.0, +0.3, -1.0)
|
112
|
+
.scale(0.3, 0.3, 0.3)
|
113
|
+
.rotate_y(-Math::PI / 8),
|
110
114
|
)
|
111
115
|
|
112
116
|
cube_r = Ra::Shape::Cube.new(
|
113
|
-
material: Ra::Material.new(
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
)
|
117
|
+
material: Ra::Material.new(
|
118
|
+
base: Ra::Pattern::Gradient.new(
|
119
|
+
color_a: Ra::Color.hex('#84cc16'),
|
120
|
+
color_b: Ra::Color.hex('#f97316'),
|
121
|
+
),
|
122
|
+
reflective: 0.5,
|
123
|
+
),
|
120
124
|
transform: Ra::Transform
|
121
|
-
.translate(-1.0,
|
122
|
-
.scale(0.3, 0.3, 0.3)
|
125
|
+
.translate(-1.0, +0.3, -1.0)
|
126
|
+
.scale(0.3, 0.3, 0.3)
|
127
|
+
.rotate_y(+Math::PI / 8),
|
123
128
|
)
|
124
129
|
|
130
|
+
lights = [
|
131
|
+
light_l,
|
132
|
+
light_r,
|
133
|
+
]
|
134
|
+
|
125
135
|
shapes = [
|
126
136
|
floor,
|
127
137
|
wall_l,
|
@@ -131,7 +141,7 @@ shapes = [
|
|
131
141
|
cube_r,
|
132
142
|
].freeze
|
133
143
|
|
134
|
-
world = Ra::World.new(
|
144
|
+
world = Ra::World.new(lights:, shapes:)
|
135
145
|
engine = Ra::Engine.new(camera:, world:)
|
136
146
|
canvas = engine.render
|
137
147
|
|
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/engine.rb
CHANGED
@@ -31,10 +31,7 @@ module Ra
|
|
31
31
|
def draw(x:, y:, canvas:)
|
32
32
|
ray = @camera.ray(x:, y:)
|
33
33
|
|
34
|
-
|
35
|
-
intersection = Intersection.hit(intersections:)
|
36
|
-
|
37
|
-
canvas[x, y] = @world.color(intersection:) if intersection
|
34
|
+
canvas[x, y] = @world.color(ray:)
|
38
35
|
end
|
39
36
|
end
|
40
37
|
end
|
data/lib/ra/intersection.rb
CHANGED
@@ -38,8 +38,9 @@ module Ra
|
|
38
38
|
point = ray.position(t:)
|
39
39
|
eyev = -ray.direction
|
40
40
|
normalv = shape.normal(point:)
|
41
|
+
reflectv = Tuple.reflect(ray.direction, normalv)
|
41
42
|
|
42
|
-
Surface.new(shape:, eyev:, normalv:, point:)
|
43
|
+
Surface.new(shape:, eyev:, normalv:, reflectv:, point:)
|
43
44
|
end
|
44
45
|
end
|
45
46
|
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
|
@@ -54,7 +53,7 @@ module Ra
|
|
54
53
|
|
55
54
|
# @return [Ra::Vector]
|
56
55
|
def reflectv
|
57
|
-
@reflectv ||= -(lightv
|
56
|
+
@reflectv ||= -Tuple.reflect(lightv, normalv)
|
58
57
|
end
|
59
58
|
|
60
59
|
# @return [Ra::Vector]
|
data/lib/ra/material.rb
CHANGED
@@ -11,17 +11,24 @@ module Ra
|
|
11
11
|
# shininess: 200,
|
12
12
|
# )
|
13
13
|
class Material
|
14
|
-
attr_accessor :base, :ambient, :diffuse, :specular, :shininess
|
14
|
+
attr_accessor :base, :ambient, :diffuse, :reflective, :specular, :shininess
|
15
15
|
|
16
16
|
# @param base [Ra::Color, Ra::Pattern:::Base]
|
17
17
|
# @param ambient [Float] between 0.0 and 1.0
|
18
18
|
# @param diffuse [Float] between 0.0 and 1.0
|
19
|
+
# @param reflective [Float] between 0.0 and 1.0
|
19
20
|
# @param specular [Float] between 0.0 and 1.0
|
20
21
|
# @param shininess [Numeric]
|
21
|
-
def initialize(base:, ambient: 0.
|
22
|
+
def initialize(base:, ambient: 0.0, diffuse: 0.8, reflective: 0.0, specular: 0.2, shininess: 80)
|
23
|
+
raise ArgumentError, "ambient=#{ambient} must be between 0 and 1" unless ambient.between?(0, 1)
|
24
|
+
raise ArgumentError, "ambient=#{diffuse} must be between 0 and 1" unless diffuse.between?(0, 1)
|
25
|
+
raise ArgumentError, "ambient=#{reflective} must be between 0 and 1" unless reflective.between?(0, 1)
|
26
|
+
raise ArgumentError, "specular=#{specular} must be between 0 and 1" unless specular.between?(0, 1)
|
27
|
+
|
22
28
|
@base = base
|
23
29
|
@ambient = ambient
|
24
30
|
@diffuse = diffuse
|
31
|
+
@reflective = reflective
|
25
32
|
@specular = specular
|
26
33
|
@shininess = shininess
|
27
34
|
end
|
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
@@ -3,22 +3,23 @@
|
|
3
3
|
module Ra
|
4
4
|
# A surface contains everything needed to apply lighting.
|
5
5
|
class Surface
|
6
|
-
attr_accessor :eyev, :normalv, :shape, :point
|
6
|
+
attr_accessor :eyev, :normalv, :reflectv, :shape, :point
|
7
7
|
|
8
8
|
# @param eyev [Vector]
|
9
9
|
# @param normalv [Vector]
|
10
10
|
# @param shape [Ra::Shape]
|
11
11
|
# @param point [Vector]
|
12
|
-
def initialize(eyev:, normalv:, shape:, point:)
|
12
|
+
def initialize(eyev:, normalv:, reflectv:, shape:, point:)
|
13
13
|
@eyev = eyev
|
14
14
|
@normalv = normalv.dot(eyev).negative? ? -normalv : +normalv
|
15
|
+
@reflectv = reflectv
|
15
16
|
@shape = shape
|
16
17
|
@point = point
|
17
18
|
end
|
18
19
|
|
19
20
|
# @return [Vector]
|
20
21
|
def hpoint
|
21
|
-
point + (normalv * EPSILON)
|
22
|
+
@hpoint ||= point + (normalv * EPSILON)
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
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]])
|
@@ -22,5 +22,12 @@ module Ra
|
|
22
22
|
Tuple::VECTOR,
|
23
23
|
]
|
24
24
|
end
|
25
|
+
|
26
|
+
# @param vector [Vector]
|
27
|
+
# @param normal [Vector]
|
28
|
+
# @return [Vector]
|
29
|
+
def self.reflect(vector, normal)
|
30
|
+
vector - (normal * 2 * vector.dot(normal))
|
31
|
+
end
|
25
32
|
end
|
26
33
|
end
|
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
|
|
@@ -26,18 +24,42 @@ module Ra
|
|
26
24
|
Intersection.hit(intersections:)
|
27
25
|
end
|
28
26
|
|
29
|
-
# @param
|
27
|
+
# @param ray [Ra::Ray]
|
30
28
|
# @return [Ra::Color]
|
31
|
-
def color(
|
29
|
+
def color(ray:, remaining: 4)
|
30
|
+
intersection = intersection(ray:)
|
31
|
+
return unless intersection
|
32
|
+
|
32
33
|
surface = intersection.surface
|
33
|
-
shadowed = shadowed?(point: surface.hpoint)
|
34
34
|
|
35
|
-
|
35
|
+
colors = @lights.map do |light|
|
36
|
+
shadowed = shadowed?(point: surface.hpoint, light:)
|
37
|
+
lighting = Lighting.new(light:, shadowed:, surface:)
|
38
|
+
lighting.color
|
39
|
+
end
|
40
|
+
|
41
|
+
colors.reduce(&:+) + reflect(surface:, remaining:)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param surface [Ra::Surface]
|
45
|
+
# @param remaining [Integer]
|
46
|
+
# @return [Ra::Color]
|
47
|
+
def reflect(surface:, remaining:)
|
48
|
+
return if remaining.zero?
|
49
|
+
|
50
|
+
material = surface.shape.material
|
51
|
+
return unless material.reflective.positive?
|
52
|
+
|
53
|
+
ray = Ray.new(origin: surface.hpoint, direction: surface.reflectv)
|
54
|
+
|
55
|
+
color = color(ray:, remaining: remaining.pred)
|
56
|
+
color * material.reflective if color
|
36
57
|
end
|
37
58
|
|
59
|
+
# @param light [Ra::Light]
|
38
60
|
# @param point [Ra::Point]
|
39
|
-
def shadowed?(point:)
|
40
|
-
vector =
|
61
|
+
def shadowed?(light:, point:)
|
62
|
+
vector = light.position - point
|
41
63
|
distance = vector.magnitude
|
42
64
|
direction = vector.normalize
|
43
65
|
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.4.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
|