ra 0.7.0 → 1.0.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 +10 -2
- data/exe/ra +38 -31
- data/lib/ra/camera.rb +35 -3
- data/lib/ra/color.rb +11 -1
- data/lib/ra/engine.rb +24 -17
- data/lib/ra/intersection.rb +31 -1
- data/lib/ra/light.rb +7 -1
- data/lib/ra/lighting.rb +11 -1
- data/lib/ra/material.rb +24 -2
- data/lib/ra/pixel.rb +27 -0
- data/lib/ra/quadratic.rb +32 -0
- data/lib/ra/ray.rb +92 -1
- data/lib/ra/shape/base.rb +2 -0
- data/lib/ra/shape/cylinder.rb +110 -0
- data/lib/ra/shape/plane.rb +2 -2
- data/lib/ra/shape/sphere.rb +4 -20
- data/lib/ra/surface.rb +19 -1
- data/lib/ra/transform.rb +10 -10
- data/lib/ra/version.rb +1 -1
- metadata +6 -4
- data/lib/ra/canvas.rb +0 -71
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7ee2d6f85d018ae7eeb70e9796973277176ddac2940dbe442355cfd8f55bd5d
|
4
|
+
data.tar.gz: 7ca5f9394c0d2e62ba5348c3047b482554ebe8437646cc1fa100eea0141fa67d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6c66f296b05b5fb771dfaab9a95b0523c5ec3563da731c6f0e177a9bc775d7309c59b3e67d2e56c4ea8d070a009e4d0b7b0d84d167bcaa2644a9886857ae3f4
|
7
|
+
data.tar.gz: c6c2465c7cc20c95aaaa3802dc0d5ad54bedfa6d8fa023bb24f9e6dfad4144b45c58ede27c998d87167ff755883afce0d7385beb0341610fa1ddadbe143a0c90
|
data/README.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Ra
|
2
2
|
|
3
|
+
[](https://github.com/ksylvest/ra/blob/main/LICENSE)
|
4
|
+
[](https://rubygems.org/gems/ra)
|
5
|
+
[](https://github.com/ksylvest/ra)
|
6
|
+
[](https://ra.ksylvest.com)
|
7
|
+
[](https://circleci.com/gh/ksylvest/ra)
|
8
|
+
[](https://codeclimate.com/github/ksylvest/ra)
|
9
|
+
[](https://codeclimate.com/github/ksylvest/ra)
|
10
|
+
|
3
11
|
Named for [Ra](https://en.wikipedia.org/wiki/Ra) - arguably the original ray tracer.
|
4
12
|
|
5
13
|
## Installation
|
@@ -12,7 +20,7 @@ gem install ra
|
|
12
20
|
|
13
21
|
```sh
|
14
22
|
ra -w 2560 -h 2048 > sample.ppm
|
15
|
-
convert -quality 80
|
23
|
+
convert -quality 80 sample.ppm sample.avif
|
16
24
|
```
|
17
25
|
|
18
|
-

|
data/exe/ra
CHANGED
@@ -10,7 +10,7 @@ config = Slop.parse(ARGV) do |options|
|
|
10
10
|
|
11
11
|
options.integer '-w', '--width', 'width', default: 2560
|
12
12
|
options.integer '-h', '--height', 'height', default: 2048
|
13
|
-
options.integer '-fov', 'degrees', default:
|
13
|
+
options.integer '-fov', 'degrees', default: 90
|
14
14
|
|
15
15
|
options.on('--help', 'help') do
|
16
16
|
Ra.logger.log(options)
|
@@ -26,13 +26,13 @@ end
|
|
26
26
|
earth = Ra::Pattern::Texture.new(path: File.join(File.dirname(__FILE__), '..', 'assets/earth.avif'))
|
27
27
|
|
28
28
|
light_l = Ra::Light.new(
|
29
|
-
position: Vector[+
|
30
|
-
intensity: Ra::Color.uniform(0.
|
29
|
+
position: Vector[+3.0, +2.0, -5.0, Ra::Tuple::POINT],
|
30
|
+
intensity: Ra::Color.uniform(0.75),
|
31
31
|
)
|
32
32
|
|
33
33
|
light_r = Ra::Light.new(
|
34
|
-
position: Vector[-
|
35
|
-
intensity: Ra::Color.uniform(0.
|
34
|
+
position: Vector[-3.0, +2.0, -5.0, Ra::Tuple::POINT],
|
35
|
+
intensity: Ra::Color.uniform(0.75),
|
36
36
|
)
|
37
37
|
|
38
38
|
camera = Ra::Camera.new(
|
@@ -41,7 +41,7 @@ camera = Ra::Camera.new(
|
|
41
41
|
fov: config[:fov] * Math::PI / 180,
|
42
42
|
transform: Ra::Transform.view(
|
43
43
|
from: Vector[0, +1.5, -4.0, Ra::Tuple::POINT],
|
44
|
-
to: Vector[0, 0, 0, Ra::Tuple::POINT],
|
44
|
+
to: Vector[0, +0.5, 0, Ra::Tuple::POINT],
|
45
45
|
up: Vector[0, 1, 0, Ra::Tuple::VECTOR],
|
46
46
|
),
|
47
47
|
)
|
@@ -57,19 +57,28 @@ floor = Ra::Shape::Plane.new(
|
|
57
57
|
reflective: 0.2,
|
58
58
|
),
|
59
59
|
transform: Ra::Transform
|
60
|
-
.rotate_y(Math::PI / 4)
|
61
|
-
.scale(0.5, 0.5, 0.5),
|
60
|
+
.rotate_y(Math::PI / 4),
|
62
61
|
)
|
63
62
|
|
64
|
-
|
63
|
+
ceiling = Ra::Shape::Plane.new(
|
65
64
|
material: Ra::Material.new(
|
66
65
|
base: Ra::Pattern::Stripes.new(
|
67
66
|
colors: [
|
68
|
-
Ra::Color.hex('#
|
67
|
+
Ra::Color.hex('#e4e4e7'),
|
69
68
|
Ra::Color.hex('#e7e5e4'),
|
70
69
|
],
|
71
70
|
),
|
72
71
|
),
|
72
|
+
transform: Ra::Transform
|
73
|
+
.translate(0, +3.0, 0)
|
74
|
+
.scale(0.2, 0.2, 0.2)
|
75
|
+
.rotate_y(Math::PI / 4),
|
76
|
+
)
|
77
|
+
|
78
|
+
wall_l = Ra::Shape::Plane.new(
|
79
|
+
material: Ra::Material.new(
|
80
|
+
base: Ra::Color.hex('#6b21a8'),
|
81
|
+
),
|
73
82
|
transform: Ra::Transform
|
74
83
|
.translate(0, 0, +3.0)
|
75
84
|
.rotate_y(-Math::PI / 4)
|
@@ -78,12 +87,7 @@ wall_l = Ra::Shape::Plane.new(
|
|
78
87
|
|
79
88
|
wall_r = Ra::Shape::Plane.new(
|
80
89
|
material: Ra::Material.new(
|
81
|
-
base: Ra::
|
82
|
-
colors: [
|
83
|
-
Ra::Color.hex('#f5f5f4'),
|
84
|
-
Ra::Color.hex('#e7e5e4'),
|
85
|
-
],
|
86
|
-
),
|
90
|
+
base: Ra::Color.hex('#9f1239'),
|
87
91
|
),
|
88
92
|
transform: Ra::Transform
|
89
93
|
.translate(0, 0, +3.0)
|
@@ -92,38 +96,41 @@ wall_r = Ra::Shape::Plane.new(
|
|
92
96
|
)
|
93
97
|
|
94
98
|
sphere = Ra::Shape::Sphere.new(
|
95
|
-
material: Ra::Material.new(
|
99
|
+
material: Ra::Material.new(
|
100
|
+
base: earth,
|
101
|
+
reflective: 0.3,
|
102
|
+
),
|
96
103
|
transform: Ra::Transform
|
97
|
-
.translate(0, +0.
|
104
|
+
.translate(0, +0.6, -2.0)
|
98
105
|
.rotate_y(Math::PI / 2)
|
99
|
-
.scale(0.
|
106
|
+
.scale(0.6, 0.6, 0.6),
|
100
107
|
)
|
101
108
|
|
102
|
-
|
109
|
+
cube = Ra::Shape::Cube.new(
|
103
110
|
material: Ra::Material.new(
|
104
111
|
base: Ra::Pattern::Gradient.new(
|
105
112
|
color_a: Ra::Color.hex('#f43f5e'),
|
106
113
|
color_b: Ra::Color.hex('#8b5cf6'),
|
107
114
|
),
|
108
|
-
reflective: 0.
|
115
|
+
reflective: 0.3,
|
109
116
|
),
|
110
117
|
transform: Ra::Transform
|
111
|
-
.translate(+1.
|
112
|
-
.scale(0.
|
118
|
+
.translate(+1.6, +0.4, -0.8)
|
119
|
+
.scale(0.4, 0.4, 0.4)
|
113
120
|
.rotate_y(-Math::PI / 8),
|
114
121
|
)
|
115
122
|
|
116
|
-
|
123
|
+
cylinder = Ra::Shape::Cylinder.new(
|
117
124
|
material: Ra::Material.new(
|
118
125
|
base: Ra::Pattern::Gradient.new(
|
119
126
|
color_a: Ra::Color.hex('#84cc16'),
|
120
127
|
color_b: Ra::Color.hex('#f97316'),
|
121
128
|
),
|
122
|
-
reflective: 0.
|
129
|
+
reflective: 0.3,
|
123
130
|
),
|
124
131
|
transform: Ra::Transform
|
125
|
-
.translate(-1.
|
126
|
-
.scale(0.
|
132
|
+
.translate(-1.6, +0.4, -0.8)
|
133
|
+
.scale(0.4, 0.4, 0.4)
|
127
134
|
.rotate_y(+Math::PI / 8),
|
128
135
|
)
|
129
136
|
|
@@ -134,15 +141,15 @@ lights = [
|
|
134
141
|
|
135
142
|
shapes = [
|
136
143
|
floor,
|
144
|
+
ceiling,
|
137
145
|
wall_l,
|
138
146
|
wall_r,
|
139
147
|
sphere,
|
140
|
-
|
141
|
-
|
148
|
+
cube,
|
149
|
+
cylinder,
|
142
150
|
].freeze
|
143
151
|
|
144
152
|
world = Ra::World.new(lights:, shapes:)
|
145
153
|
engine = Ra::Engine.new(camera:, world:)
|
146
|
-
canvas = engine.render
|
147
154
|
|
148
|
-
Ra.logger.log(
|
155
|
+
engine.ppm { |text| Ra.logger.log(text) }
|
data/lib/ra/camera.rb
CHANGED
@@ -24,15 +24,31 @@ module Ra
|
|
24
24
|
# dimensions rays are cast to the center of pixels evenly distrubted across
|
25
25
|
# the screen.
|
26
26
|
class Camera
|
27
|
-
|
27
|
+
include Enumerable
|
28
|
+
|
29
|
+
# @!attribute h
|
30
|
+
# @return [Integer]
|
31
|
+
attr_accessor :h
|
32
|
+
|
33
|
+
# @!attribute w
|
34
|
+
# @return [Integer]
|
35
|
+
attr_accessor :w
|
36
|
+
|
37
|
+
# @!attribute fov
|
38
|
+
# @return [Numeric]
|
39
|
+
attr_accessor :fov
|
40
|
+
|
41
|
+
# @!attribute transform
|
42
|
+
# @return [Ra::Transform]
|
43
|
+
attr_accessor :transform
|
28
44
|
|
29
45
|
DEFAULT_W = 1280
|
30
46
|
DEFAULT_H = 1024
|
31
47
|
DEFAULT_FOV = Math::PI / 3
|
32
48
|
|
33
49
|
# @param transform [Ra::Transform]
|
34
|
-
# @param h [
|
35
|
-
# @param w [
|
50
|
+
# @param h [Integer]
|
51
|
+
# @param w [Integer]
|
36
52
|
# @param fov [Numeric]
|
37
53
|
def initialize(transform: Transform::IDENTITY, h: DEFAULT_H, w: DEFAULT_W, fov: DEFAULT_FOV)
|
38
54
|
@transform = transform
|
@@ -41,6 +57,22 @@ module Ra
|
|
41
57
|
@fov = fov
|
42
58
|
end
|
43
59
|
|
60
|
+
# @yield [y, x, ray] y, x, ray
|
61
|
+
# @yieldparam [Integer] y
|
62
|
+
# @yieldparam [Integer] x
|
63
|
+
# @yieldparam [Ra::Ray] ray
|
64
|
+
def each
|
65
|
+
@h.times do |y|
|
66
|
+
@w.times do |x|
|
67
|
+
ray = ray(
|
68
|
+
y:,
|
69
|
+
x:,
|
70
|
+
)
|
71
|
+
yield(y, x, ray)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
44
76
|
# @param x [Numeric]
|
45
77
|
# @param y [Numeric]
|
46
78
|
# @return [Ra::Ray]
|
data/lib/ra/color.rb
CHANGED
@@ -17,7 +17,17 @@ module Ra
|
|
17
17
|
# )
|
18
18
|
# color.ppm == "128 179 230"
|
19
19
|
class Color
|
20
|
-
|
20
|
+
# @!attribute r
|
21
|
+
# @return [Numeric]
|
22
|
+
attr_accessor :r
|
23
|
+
|
24
|
+
# @!attribute g
|
25
|
+
# @return [Numeric]
|
26
|
+
attr_accessor :g
|
27
|
+
|
28
|
+
# @!attribute b
|
29
|
+
# @return [Numeric]
|
30
|
+
attr_accessor :b
|
21
31
|
|
22
32
|
PRECISION = 255
|
23
33
|
|
data/lib/ra/engine.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Ra
|
4
|
-
# An engine takes
|
4
|
+
# An engine takes a world / camera and generates a PPM.
|
5
5
|
class Engine
|
6
|
+
include Enumerable
|
7
|
+
|
6
8
|
PRECISION = 255
|
9
|
+
PORCESSES = 8
|
10
|
+
|
11
|
+
PPM_VERSION = 'P3'
|
12
|
+
PPM_DEFAULT = '0 0 0'
|
7
13
|
|
8
14
|
# @param world [Ra::World]
|
9
15
|
# @param camera [Ra::Camera]
|
@@ -12,26 +18,27 @@ module Ra
|
|
12
18
|
@camera = camera
|
13
19
|
end
|
14
20
|
|
15
|
-
# @
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
22
|
-
end
|
21
|
+
# @yield [pixel]
|
22
|
+
# @yieldparam [Ra::Pixel] pixel
|
23
|
+
def each
|
24
|
+
@camera.each do |y, x, ray|
|
25
|
+
color = @world.color(ray:)
|
26
|
+
yield(Ra::Pixel.new(x:, y:, color:))
|
23
27
|
end
|
24
28
|
end
|
25
29
|
|
26
|
-
|
30
|
+
# @yield [text]
|
31
|
+
# @yieldparam [String] text
|
32
|
+
def ppm
|
33
|
+
yield(<<~PPM)
|
34
|
+
#{PPM_VERSION}
|
35
|
+
#{@camera.w} #{@camera.h}
|
36
|
+
#{Color::PRECISION}
|
37
|
+
PPM
|
27
38
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
def draw(x:, y:, canvas:)
|
32
|
-
ray = @camera.ray(x:, y:)
|
33
|
-
|
34
|
-
canvas[x, y] = @world.color(ray:)
|
39
|
+
each do |pixel|
|
40
|
+
yield(pixel.color ? pixel.color.ppm : PPM_DEFAULT)
|
41
|
+
end
|
35
42
|
end
|
36
43
|
end
|
37
44
|
end
|
data/lib/ra/intersection.rb
CHANGED
@@ -3,7 +3,17 @@
|
|
3
3
|
module Ra
|
4
4
|
# An intersection tracks the time t at which a shape is intersected by a ray.
|
5
5
|
class Intersection
|
6
|
-
|
6
|
+
# @!attribute t
|
7
|
+
# @return [Numeric]
|
8
|
+
attr_accessor :t
|
9
|
+
|
10
|
+
# @attribute ray
|
11
|
+
# @return [Ra::Ray]
|
12
|
+
attr_accessor :ray
|
13
|
+
|
14
|
+
# @attribute shape
|
15
|
+
# @return [Ra::Shape::Base]
|
16
|
+
attr_accessor :shape
|
7
17
|
|
8
18
|
# @param intersections Array<Ra::Intersection>
|
9
19
|
# @return [Ra::Intersection, nil]
|
@@ -42,5 +52,25 @@ module Ra
|
|
42
52
|
|
43
53
|
Surface.new(shape:, eyev:, normalv:, reflectv:, point:)
|
44
54
|
end
|
55
|
+
|
56
|
+
# @return [Vector]
|
57
|
+
def position
|
58
|
+
@position ||= @ray.position(t: @t)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Numeric]
|
62
|
+
def x
|
63
|
+
position[0]
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Numeric]
|
67
|
+
def y
|
68
|
+
position[1]
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Numeric]
|
72
|
+
def z
|
73
|
+
position[1]
|
74
|
+
end
|
45
75
|
end
|
46
76
|
end
|
data/lib/ra/light.rb
CHANGED
@@ -9,7 +9,13 @@ module Ra
|
|
9
9
|
# position: Vector[0, 0, 0, Ra::Tuple::POINT],
|
10
10
|
# )
|
11
11
|
class Light
|
12
|
-
|
12
|
+
# @!attribute intensity
|
13
|
+
# @return [Ra::Color]
|
14
|
+
attr_accessor :intensity
|
15
|
+
|
16
|
+
# @!attribute position
|
17
|
+
# @return [Vector]
|
18
|
+
attr_accessor :position
|
13
19
|
|
14
20
|
# @param intensity [Ra::Color]
|
15
21
|
# @param position [Vector]
|
data/lib/ra/lighting.rb
CHANGED
@@ -3,7 +3,17 @@
|
|
3
3
|
module Ra
|
4
4
|
# Lighting encaspulates a [Phong Reflection Model](https://en.wikipedia.org/wiki/phong_reflection_model).
|
5
5
|
class Lighting
|
6
|
-
|
6
|
+
# @!attribute surface
|
7
|
+
# @return [Ra::Surface]
|
8
|
+
attr_accessor :surface
|
9
|
+
|
10
|
+
# @!attribute shadowed
|
11
|
+
# @return [Boolean]
|
12
|
+
attr_accessor :shadowed
|
13
|
+
|
14
|
+
# @!attribute light
|
15
|
+
# @return [Ra::Light]
|
16
|
+
attr_accessor :light
|
7
17
|
|
8
18
|
# @param light [Ra::Light]
|
9
19
|
# @param surface [Ra::Surface]
|
data/lib/ra/material.rb
CHANGED
@@ -11,14 +11,36 @@ module Ra
|
|
11
11
|
# shininess: 200,
|
12
12
|
# )
|
13
13
|
class Material
|
14
|
-
|
14
|
+
# @!attribute base
|
15
|
+
# @return [Ra::Color, Ra::Pattern::Base]
|
16
|
+
attr_accessor :base
|
17
|
+
|
18
|
+
# @!attribute ambient
|
19
|
+
# @return [Float]
|
20
|
+
attr_accessor :ambient
|
21
|
+
|
22
|
+
# @!attribute diffuse
|
23
|
+
# @return [Float]
|
24
|
+
attr_accessor :diffuse
|
25
|
+
|
26
|
+
# @!attribute reflective
|
27
|
+
# @return [Float]
|
28
|
+
attr_accessor :reflective
|
29
|
+
|
30
|
+
# @!attribute specular
|
31
|
+
# @return [Float]
|
32
|
+
attr_accessor :specular
|
33
|
+
|
34
|
+
# @!attribute shininess
|
35
|
+
# @return [Integer]
|
36
|
+
attr_accessor :shininess
|
15
37
|
|
16
38
|
# @param base [Ra::Color, Ra::Pattern:::Base]
|
17
39
|
# @param ambient [Float] between 0.0 and 1.0
|
18
40
|
# @param diffuse [Float] between 0.0 and 1.0
|
19
41
|
# @param reflective [Float] between 0.0 and 1.0
|
20
42
|
# @param specular [Float] between 0.0 and 1.0
|
21
|
-
# @param shininess [
|
43
|
+
# @param shininess [Integer]
|
22
44
|
def initialize(base:, ambient: 0.0, diffuse: 0.8, reflective: 0.0, specular: 0.2, shininess: 80)
|
23
45
|
raise ArgumentError, "ambient=#{ambient} must be between 0 and 1" unless ambient.between?(0, 1)
|
24
46
|
raise ArgumentError, "ambient=#{diffuse} must be between 0 and 1" unless diffuse.between?(0, 1)
|
data/lib/ra/pixel.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ra
|
4
|
+
# A pixel is a point in an image. It pairs the coordinates (x,y) with the color for that point.
|
5
|
+
class Pixel
|
6
|
+
# @!attribute x
|
7
|
+
# @return [Integer]
|
8
|
+
attr_accessor :x
|
9
|
+
|
10
|
+
# @!attribute y
|
11
|
+
# @return [Integer]
|
12
|
+
attr_accessor :y
|
13
|
+
|
14
|
+
# @!attribute color
|
15
|
+
# @return [Ra::Color]
|
16
|
+
attr_accessor :color
|
17
|
+
|
18
|
+
# @param x [Integer]
|
19
|
+
# @param y [Integer]
|
20
|
+
# @param color [Ra::Color]
|
21
|
+
def initialize(x:, y:, color:)
|
22
|
+
@x = x
|
23
|
+
@y = y
|
24
|
+
@color = color
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/ra/quadratic.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ra
|
4
|
+
# A solver for all things quadratic. Given the equation:
|
5
|
+
#
|
6
|
+
# ax² + bx +c = 0
|
7
|
+
#
|
8
|
+
# The solution for `x`` can be found via:
|
9
|
+
#
|
10
|
+
# (-b ± √(b² - 4ac)) / (2a)
|
11
|
+
#
|
12
|
+
# No solution is defined when the discriminant (`b² - 4ac`) is negative or `a` is zero.
|
13
|
+
module Quadratic
|
14
|
+
# (-b ± √(b² - 4ac)) / (2a)
|
15
|
+
#
|
16
|
+
# @param a [Numeric]
|
17
|
+
# @param b [Numeric]
|
18
|
+
# @param c [Numeric]
|
19
|
+
# @return [Array<Numeric>]
|
20
|
+
def self.solve(a:, b:, c:)
|
21
|
+
return [] if a.zero?
|
22
|
+
|
23
|
+
discriminant = (b**2) - (4 * a * c)
|
24
|
+
return [] if discriminant.negative?
|
25
|
+
|
26
|
+
[
|
27
|
+
(-b - Math.sqrt(discriminant)) / (2 * a),
|
28
|
+
(-b + Math.sqrt(discriminant)) / (2 * a),
|
29
|
+
]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/ra/ray.rb
CHANGED
@@ -21,7 +21,13 @@ module Ra
|
|
21
21
|
# )
|
22
22
|
# ray.transform(transform: Ra::Transform.scale(1, 2, 3))
|
23
23
|
class Ray
|
24
|
-
|
24
|
+
# @!attribute origin
|
25
|
+
# @return [Vector]
|
26
|
+
attr_accessor :origin
|
27
|
+
|
28
|
+
# @!attribute direction
|
29
|
+
# @return [Vector]
|
30
|
+
attr_accessor :direction
|
25
31
|
|
26
32
|
# @param origin [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::POINT]
|
27
33
|
# @param direction [Vector] e.g. Vector[1, 2, 3, Ra::Tuple::VECTOR]
|
@@ -49,5 +55,90 @@ module Ra
|
|
49
55
|
def ==(other)
|
50
56
|
origin == other.origin && direction == other.direction
|
51
57
|
end
|
58
|
+
|
59
|
+
# @param t [Numeric]
|
60
|
+
# @return [Numeric]
|
61
|
+
def x(t:)
|
62
|
+
origin_x + (direction_x * t)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param t [Numeric]
|
66
|
+
# @return [Numeric]
|
67
|
+
def y(t:)
|
68
|
+
origin_y + (direction_y * t)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param t [Numeric]
|
72
|
+
# @return [Numeric]
|
73
|
+
def z(t:)
|
74
|
+
origin_z + (direction_z * t)
|
75
|
+
end
|
76
|
+
|
77
|
+
# The time t when the ray is at x
|
78
|
+
# @param x [Numeric]
|
79
|
+
# @return [Numeric]
|
80
|
+
def t_x(x)
|
81
|
+
return if direction_x.zero?
|
82
|
+
|
83
|
+
(x - origin_x) / direction_x
|
84
|
+
end
|
85
|
+
|
86
|
+
# The time t when the ray is at y
|
87
|
+
# @param y [Numeric]
|
88
|
+
# @return [Numeric, nil]
|
89
|
+
def t_y(y)
|
90
|
+
return if direction_y.zero?
|
91
|
+
|
92
|
+
(y - origin_y) / direction_y
|
93
|
+
end
|
94
|
+
|
95
|
+
# The time t when the ray is at z
|
96
|
+
# @param z [Numeric]
|
97
|
+
# @return [Numeric, nil]
|
98
|
+
def t_z(z)
|
99
|
+
return if direction_z.zero?
|
100
|
+
|
101
|
+
(z - origin_z) / direction_z
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [Numeric]
|
105
|
+
def direction_x
|
106
|
+
@direction[0]
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Numeric]
|
110
|
+
def direction_y
|
111
|
+
@direction[1]
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Numeric]
|
115
|
+
def direction_z
|
116
|
+
@direction[2]
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [0] 0 = vector / 1 = point
|
120
|
+
def direction_w
|
121
|
+
@direction[3]
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [Numeric]
|
125
|
+
def origin_x
|
126
|
+
@origin[0]
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [Numeric]
|
130
|
+
def origin_y
|
131
|
+
@origin[1]
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Numeric]
|
135
|
+
def origin_z
|
136
|
+
@origin[2]
|
137
|
+
end
|
138
|
+
|
139
|
+
# @return [1] 0 = vector / 1 = point
|
140
|
+
def origin_w
|
141
|
+
@origin[3]
|
142
|
+
end
|
52
143
|
end
|
53
144
|
end
|
data/lib/ra/shape/base.rb
CHANGED
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ra
|
4
|
+
module Shape
|
5
|
+
# A cylinder at origin <0,0,0> with a radius 1. A cylinder surface is defined:
|
6
|
+
#
|
7
|
+
# (x² + z² = radius² AND y BETWEEN ±1) OR (y = ±1 AND x² + z² < radius)
|
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
|
+
# Substituting `x` / `y` / `z` allows for solving for `t`:
|
16
|
+
#
|
17
|
+
# (origin.x + direction.x * t)² + (origin.z + direction.z * t)² = 1
|
18
|
+
#
|
19
|
+
# Simplifying gives a quadratic formula with terms defined as:
|
20
|
+
#
|
21
|
+
# a = direction.x² + direction.z²
|
22
|
+
# b = 2 * ((origin.x * direction.x) + (origin.z * direction.z))
|
23
|
+
# c = origin.x² + origin.z² - 1
|
24
|
+
# discriminant = b² - 4ac
|
25
|
+
# t = (-b ± √discriminant) / (2a)
|
26
|
+
#
|
27
|
+
# A discriminant <0 indicates the ray does not intersect the sphere.
|
28
|
+
class Cylinder < Base
|
29
|
+
MIN_Y = -1
|
30
|
+
MAX_Y = +1
|
31
|
+
RADIUS = 1
|
32
|
+
|
33
|
+
# @param point [Vector] <x, y, z, Tuple::POINT>
|
34
|
+
# @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
35
|
+
def uv_point(point:)
|
36
|
+
x = point[0]
|
37
|
+
y = point[1]
|
38
|
+
z = point[2]
|
39
|
+
|
40
|
+
theta = Math.atan2(x, z)
|
41
|
+
|
42
|
+
u = 1 - ((theta / (2 * Math::PI)) + 0.5)
|
43
|
+
v = y % 1.0
|
44
|
+
|
45
|
+
Vector[u, v]
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param ray [Ra::Ray] local
|
49
|
+
# @return [Array<Numeric>]
|
50
|
+
def t_intersect(ray:)
|
51
|
+
t_intersect_caps(ray:) +
|
52
|
+
t_intersect_side(ray:).filter { |t| ray.y(t:) > MIN_Y && ray.y(t:) < MAX_Y }
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param point [Vector]
|
56
|
+
# @return [Ra::Tuple]
|
57
|
+
def l_normal(point:)
|
58
|
+
x = point[0]
|
59
|
+
y = point[1]
|
60
|
+
z = point[2]
|
61
|
+
|
62
|
+
distance = (x * x) + (z * z)
|
63
|
+
|
64
|
+
if distance < 1
|
65
|
+
return Vector[0, +1, 0, Tuple::VECTOR] if y >= MAX_Y - EPSILON
|
66
|
+
return Vector[0, -1, 0, Tuple::VECTOR] if y <= MIN_Y + EPSILON
|
67
|
+
end
|
68
|
+
|
69
|
+
Vector[point[0], 0, point[2], Tuple::VECTOR]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# @param ray [Ra::Ray] local
|
75
|
+
# @return [Array<Numeric>]
|
76
|
+
def t_intersect_side(ray:)
|
77
|
+
direction_x = ray.direction_x
|
78
|
+
direction_z = ray.direction_z
|
79
|
+
origin_x = ray.origin_x
|
80
|
+
origin_z = ray.origin_z
|
81
|
+
|
82
|
+
Quadratic.solve(
|
83
|
+
a: (direction_x * direction_x) + (direction_z * direction_z),
|
84
|
+
b: 2 * ((origin_x * direction_x) + (origin_z * direction_z)),
|
85
|
+
c: ((origin_x * origin_x) + (origin_z * origin_z)) - RADIUS,
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def t_intersect_caps(ray:)
|
90
|
+
[
|
91
|
+
t_intersect_y(ray:, y: MIN_Y),
|
92
|
+
t_intersect_y(ray:, y: MAX_Y),
|
93
|
+
].compact
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param ray [Ra::Ray] local
|
97
|
+
# @return [Numeric, nil]
|
98
|
+
def t_intersect_y(ray:, y:)
|
99
|
+
t = ray.t_y(y)
|
100
|
+
return unless t
|
101
|
+
|
102
|
+
point = ray.position(t:)
|
103
|
+
|
104
|
+
return if (point[0] * point[0]) + (point[2] * point[2]) > RADIUS
|
105
|
+
|
106
|
+
t
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/ra/shape/plane.rb
CHANGED
@@ -30,8 +30,8 @@ module Ra
|
|
30
30
|
# @param ray [Ra::Ray] local
|
31
31
|
# @return [Array<Numeric>]
|
32
32
|
def t_intersect(ray:)
|
33
|
-
origin_y = ray.
|
34
|
-
direction_y = ray.
|
33
|
+
origin_y = ray.origin_y
|
34
|
+
direction_y = ray.direction_y
|
35
35
|
|
36
36
|
return [] if direction_y.abs < EPSILON
|
37
37
|
|
data/lib/ra/shape/sphere.rb
CHANGED
@@ -32,6 +32,8 @@ module Ra
|
|
32
32
|
#
|
33
33
|
# A discriminant <0 indicates the ray does not intersect the sphere.
|
34
34
|
class Sphere < Base
|
35
|
+
RADIUS = 1
|
36
|
+
|
35
37
|
# @param point [Vector] <x, y, z, Tuple::POINT>
|
36
38
|
# @return [Vector] <u = 0.0..1.0, v = 0.0..1.0>
|
37
39
|
def uv_point(point:)
|
@@ -55,10 +57,10 @@ module Ra
|
|
55
57
|
origin = ray.origin - Vector[0, 0, 0, Ra::Tuple::POINT]
|
56
58
|
direction = ray.direction
|
57
59
|
|
58
|
-
|
60
|
+
Quadratic.solve(
|
59
61
|
a: direction.dot(direction),
|
60
62
|
b: 2 * direction.dot(origin),
|
61
|
-
c: origin.dot(origin) -
|
63
|
+
c: origin.dot(origin) - RADIUS,
|
62
64
|
)
|
63
65
|
end
|
64
66
|
|
@@ -67,24 +69,6 @@ module Ra
|
|
67
69
|
def l_normal(point:)
|
68
70
|
point - Vector[0, 0, 0, Ra::Tuple::VECTOR]
|
69
71
|
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
72
|
end
|
89
73
|
end
|
90
74
|
end
|
data/lib/ra/surface.rb
CHANGED
@@ -3,7 +3,25 @@
|
|
3
3
|
module Ra
|
4
4
|
# A surface contains everything needed to apply lighting.
|
5
5
|
class Surface
|
6
|
-
|
6
|
+
# @!attribute eyev
|
7
|
+
# @return [Vector]
|
8
|
+
attr_accessor :eyev
|
9
|
+
|
10
|
+
# @!attribute normalv
|
11
|
+
# @return [Vector]
|
12
|
+
attr_accessor :normalv
|
13
|
+
|
14
|
+
# @!attribute reflectv
|
15
|
+
# @return [Vector]
|
16
|
+
attr_accessor :reflectv
|
17
|
+
|
18
|
+
# @!attribute shape
|
19
|
+
# @return [Ra::Shape]
|
20
|
+
attr_accessor :shape
|
21
|
+
|
22
|
+
# @!attribute point
|
23
|
+
# @return [Vector]
|
24
|
+
attr_accessor :point
|
7
25
|
|
8
26
|
# @param eyev [Vector]
|
9
27
|
# @param normalv [Vector]
|
data/lib/ra/transform.rb
CHANGED
@@ -94,34 +94,34 @@ module Ra
|
|
94
94
|
# @param x [Numeric]
|
95
95
|
# @param y [Numeric]
|
96
96
|
# @param z [Numeric]
|
97
|
-
def translate(
|
98
|
-
self * self.class.translate(
|
97
|
+
def translate(x, y, z)
|
98
|
+
self * self.class.translate(x, y, z)
|
99
99
|
end
|
100
100
|
|
101
101
|
# @return [Ra::Transform]
|
102
102
|
# @param x [Numeric]
|
103
103
|
# @param y [Numeric]
|
104
104
|
# @param z [Numeric]
|
105
|
-
def scale(
|
106
|
-
self * self.class.scale(
|
105
|
+
def scale(x, y, z)
|
106
|
+
self * self.class.scale(x, y, z)
|
107
107
|
end
|
108
108
|
|
109
109
|
# @return [Ra::Transform]
|
110
110
|
# @param rotation [Numeric]
|
111
|
-
def rotate_x(
|
112
|
-
self * self.class.rotate_x(
|
111
|
+
def rotate_x(rotation)
|
112
|
+
self * self.class.rotate_x(rotation)
|
113
113
|
end
|
114
114
|
|
115
115
|
# @return [Ra::Transform]
|
116
116
|
# @param rotation [Numeric]
|
117
|
-
def rotate_y(
|
118
|
-
self * self.class.rotate_y(
|
117
|
+
def rotate_y(rotation)
|
118
|
+
self * self.class.rotate_y(rotation)
|
119
119
|
end
|
120
120
|
|
121
121
|
# @return [Ra::Transform]
|
122
122
|
# @param rotation [Numeric]
|
123
|
-
def rotate_z(
|
124
|
-
self * self.class.rotate_z(
|
123
|
+
def rotate_z(rotation)
|
124
|
+
self * self.class.rotate_z(rotation)
|
125
125
|
end
|
126
126
|
|
127
127
|
# Avoid re-computing a transform inverse by memoizing.
|
data/lib/ra/version.rb
CHANGED
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: 1.0.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:
|
11
|
+
date: 2024-11-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: matrix
|
@@ -79,7 +79,6 @@ files:
|
|
79
79
|
- exe/ra
|
80
80
|
- lib/ra.rb
|
81
81
|
- lib/ra/camera.rb
|
82
|
-
- lib/ra/canvas.rb
|
83
82
|
- lib/ra/color.rb
|
84
83
|
- lib/ra/engine.rb
|
85
84
|
- lib/ra/intersection.rb
|
@@ -93,9 +92,12 @@ files:
|
|
93
92
|
- lib/ra/pattern/rings.rb
|
94
93
|
- lib/ra/pattern/stripes.rb
|
95
94
|
- lib/ra/pattern/texture.rb
|
95
|
+
- lib/ra/pixel.rb
|
96
|
+
- lib/ra/quadratic.rb
|
96
97
|
- lib/ra/ray.rb
|
97
98
|
- lib/ra/shape/base.rb
|
98
99
|
- lib/ra/shape/cube.rb
|
100
|
+
- lib/ra/shape/cylinder.rb
|
99
101
|
- lib/ra/shape/plane.rb
|
100
102
|
- lib/ra/shape/sphere.rb
|
101
103
|
- lib/ra/surface.rb
|
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
125
|
- !ruby/object:Gem::Version
|
124
126
|
version: '0'
|
125
127
|
requirements: []
|
126
|
-
rubygems_version: 3.
|
128
|
+
rubygems_version: 3.5.23
|
127
129
|
signing_key:
|
128
130
|
specification_version: 4
|
129
131
|
summary: A graphics library written for fun.
|
data/lib/ra/canvas.rb
DELETED
@@ -1,71 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Ra
|
4
|
-
# A canvas is used to track pixels at <x,y> coordinates. It is given a w / h
|
5
|
-
# and on initialization allocates a w by h collection of pixels. For example,
|
6
|
-
# defining a basic black and white checkboard canvas with altering squares
|
7
|
-
# then saving as [PPM](https://netpbm.sourceforge.net/doc/ppm.html):
|
8
|
-
#
|
9
|
-
# canvas = Ra::Canvas.new(w: 4, h: 5, precision: 15)
|
10
|
-
# canvas.w.times do |x|
|
11
|
-
# canvas.h.times do |y|
|
12
|
-
# canvas[x,y] = (x + y) % 2 == 0 ? Ra::Color.black : Ra::Color.white
|
13
|
-
# end
|
14
|
-
# end
|
15
|
-
# canvas.ppm
|
16
|
-
class Canvas
|
17
|
-
attr_accessor :w, :h, :precision
|
18
|
-
|
19
|
-
PPM_VERSION = 'P3'
|
20
|
-
|
21
|
-
# @param w [Integer]
|
22
|
-
# @param h [Integer]
|
23
|
-
# @param precision [Integer]
|
24
|
-
def initialize(w:, h:, precision: Color::PRECISION)
|
25
|
-
@w = w
|
26
|
-
@h = h
|
27
|
-
@precision = precision
|
28
|
-
@pixels = Array.new(w) { Array.new(h) }
|
29
|
-
end
|
30
|
-
|
31
|
-
# @param x [Integer]
|
32
|
-
# @param y [Integer]
|
33
|
-
def [](x, y)
|
34
|
-
raise ArgumentError, "x=#{x} cannot be negative" if x.negative?
|
35
|
-
raise ArgumentError, "y=#{y} cannot be negative" if y.negative?
|
36
|
-
raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
|
37
|
-
raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
|
38
|
-
|
39
|
-
@pixels[x][y] || Color.black
|
40
|
-
end
|
41
|
-
|
42
|
-
# @param x [Integer]
|
43
|
-
# @param y [Integer]
|
44
|
-
# @param color [Ra::Color]
|
45
|
-
def []=(x, y, color)
|
46
|
-
raise ArgumentError, "x=#{x} cannot be negative" if x.negative?
|
47
|
-
raise ArgumentError, "y=#{y} cannot be negative" if y.negative?
|
48
|
-
raise ArgumentError, "x=#{x} must be < #{@w}" unless x < @w
|
49
|
-
raise ArgumentError, "y=#{y} must be < #{@h}" unless y < @h
|
50
|
-
|
51
|
-
@pixels[x][y] = color
|
52
|
-
end
|
53
|
-
|
54
|
-
# @return [String]
|
55
|
-
def ppm
|
56
|
-
buffer = String.new(<<~PPM, encoding: 'ascii')
|
57
|
-
#{PPM_VERSION}
|
58
|
-
#{@w} #{@h}
|
59
|
-
#{@precision}
|
60
|
-
PPM
|
61
|
-
|
62
|
-
@h.times do |y|
|
63
|
-
@w.times do |x|
|
64
|
-
buffer << (self[x, y].ppm(precision: @precision)) << "\n"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
buffer
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|