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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0132d533f2f492c3e1816a06c4e4ff8dbed1be99720685e503d7db1bd1f95169
4
- data.tar.gz: 8a08e49e2fcd0b13369814633f44d7ae8eddd62364b56ff3b7d1dc067fda8d5e
3
+ metadata.gz: b56b6d752ce23d6484526702a0702d865bfad49f0209a40a8e597adf7a3c1b13
4
+ data.tar.gz: e45c8ddc28b1723f9e2a114386a2a0927026e9cf2cb54a38f438f3583d84710c
5
5
  SHA512:
6
- metadata.gz: 700954bb31ae48227519b590f50ea0337c05a54896276dc0efcc84010385bd0d5373621c00788b5de8558f97f5cdc679ea48cfd584c45887d0c0175fd52279e4
7
- data.tar.gz: 5fcf6251eff2401b4fde9a47ac4fcd2f05a8b69b81586bd433aa7d4bae52165fbc879ac209fd64bb3dc7bb65d4c350cc9cbecb40e44c3d92ba76abe433216300
6
+ metadata.gz: 0b2723e84ab6a64a4036615d298e733714838a754aecd59e0146eef97bd7b3c01374a3a9b4e145e9bb3ac437fe6499b41d819bf3fb79be92494bd2a2629c6f39
7
+ data.tar.gz: 1f74e78ddc9fbd2c61e231601d08519a8279279418ce8df0e8d6528a183412e09bbbfd138cbb3cd15fcb96979cac30b312520b3694b2c1232d394a1598dcfa57
data/README.md CHANGED
@@ -1,31 +1,18 @@
1
1
  # Ra
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ra`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ Named for [Ra](https://en.wikipedia.org/wiki/Ra) - arguably the original ray tracer.
6
4
 
7
5
  ## Installation
8
6
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
- Install the gem and add to the application's Gemfile by executing:
12
-
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
14
-
15
- If bundler is not being used to manage dependencies, install the gem by executing:
16
-
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
7
+ ```sh
8
+ gem install ra
9
+ ```
18
10
 
19
11
  ## Usage
20
12
 
21
- TODO: Write usage instructions here
22
-
23
- ## Development
24
-
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
-
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
-
29
- ## Contributing
13
+ ```sh
14
+ ra -w 2560 -h 2048 > sample.ppm
15
+ convert -quality 80 sample.ppm sample.avif
16
+ ```
30
17
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ra.
18
+ ![Sample](./sample.avif)
data/exe/ra ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'ra'
6
+ require 'slop'
7
+
8
+ config = Slop.parse(ARGV) do |options|
9
+ options.banner = 'Usage: ra -w 2560 -h 2048 | convert - sample.avif'
10
+
11
+ options.integer '-w', '--width', 'width', default: 1280
12
+ options.integer '-h', '--height', 'height', default: 1024
13
+ options.integer '-fov', 'degrees', default: 60
14
+
15
+ options.on('--help', 'help') do
16
+ Ra.logger.log(options)
17
+ exit
18
+ end
19
+
20
+ options.on('--version', 'version') do
21
+ Ra.logger.log(Ra::VERSION)
22
+ exit
23
+ end
24
+ end
25
+
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, +7, -9, Ra::Tuple::POINT],
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),
36
+ )
37
+
38
+ camera = Ra::Camera.new(
39
+ w: config[:w],
40
+ h: config[:h],
41
+ fov: config[:fov] * Math::PI / 180,
42
+ transform: Ra::Transform.view(
43
+ from: Vector[0, +1.5, -5.0, Ra::Tuple::POINT],
44
+ to: Vector[0, 0, +1.0, Ra::Tuple::POINT],
45
+ up: Vector[0, 1, 0, Ra::Tuple::VECTOR],
46
+ ),
47
+ )
48
+
49
+ floor = Ra::Shape::Plane.new(
50
+ material: Ra::Material.new(base: Ra::Pattern::Checkers.new(
51
+ colors: [
52
+ Ra::Color.hex('#e2e8f0'),
53
+ Ra::Color.hex('#94a3b8'),
54
+ ],
55
+ )),
56
+ )
57
+
58
+ wall_l = Ra::Shape::Plane.new(
59
+ material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
60
+ colors: [
61
+ Ra::Color.hex('#94a3b8'),
62
+ Ra::Color.hex('#475569'),
63
+ ],
64
+ )),
65
+ transform: Ra::Transform
66
+ .translate(0, 0, +5.0)
67
+ .rotate_y(-Math::PI / 4)
68
+ .rotate_x(Math::PI / 2),
69
+ )
70
+
71
+ wall_r = Ra::Shape::Plane.new(
72
+ material: Ra::Material.new(base: Ra::Pattern::Stripes.new(
73
+ colors: [
74
+ Ra::Color.hex('#94a3b8'),
75
+ Ra::Color.hex('#475569'),
76
+ ],
77
+ )),
78
+ transform: Ra::Transform
79
+ .translate(0, 0, +5.0)
80
+ .rotate_y(Math::PI / 4)
81
+ .rotate_x(Math::PI / 2),
82
+ )
83
+
84
+ sphere = Ra::Shape::Sphere.new(
85
+ material: Ra::Material.new(base: earth),
86
+ transform: Ra::Transform
87
+ .translate(0, +0.5, -2.0)
88
+ .rotate_y(Math::PI / 2)
89
+ .scale(0.5, 0.5, 0.5),
90
+ )
91
+
92
+ cube_l = Ra::Shape::Cube.new(
93
+ material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
94
+ color_a: Ra::Color.hex('#f43f5e'),
95
+ color_b: Ra::Color.hex('#8b5cf6'),
96
+ )),
97
+ transform: Ra::Transform
98
+ .translate(+1.0, +0.3, -1.5)
99
+ .scale(0.3, 0.3, 0.3),
100
+ )
101
+
102
+ cube_r = Ra::Shape::Cube.new(
103
+ material: Ra::Material.new(base: Ra::Pattern::Gradient.new(
104
+ color_a: Ra::Color.hex('#84cc16'),
105
+ color_b: Ra::Color.hex('#f97316'),
106
+ )),
107
+ transform: Ra::Transform
108
+ .translate(-1.0, +0.3, -1.5)
109
+ .scale(0.3, 0.3, 0.3),
110
+ )
111
+
112
+ lights = [
113
+ light_l,
114
+ light_r,
115
+ ]
116
+
117
+ shapes = [
118
+ floor,
119
+ wall_l,
120
+ wall_r,
121
+ sphere,
122
+ cube_l,
123
+ cube_r,
124
+ ].freeze
125
+
126
+ world = Ra::World.new(lights:, shapes:)
127
+ engine = Ra::Engine.new(camera:, world:)
128
+ canvas = engine.render
129
+
130
+ Ra.logger.log(canvas.ppm)
data/lib/ra/camera.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A camera produces rays used to generate pixels. By convention, the camera
5
+ # is positioned at the point <x=0,y=0,z=0>. The rays target an imaginary
6
+ # screen that exists at z=-1. The x,y values visible on the screen depend on
7
+ # the FOV and the width / height of the desired image.
8
+ #
9
+ # A FOV represents the angel of the world visible to the camera. A default
10
+ # FOV is 90 degrees. This results in 2.0 by 2.0 of the world visible at z=-1.
11
+ #
12
+ # A bigger FOV increases what in the world is visible to the camera. When FOV
13
+ # is 120 degrees then ~3.5 by ~3.5 world view visible through z=-1.
14
+ #
15
+ # A smaller FOV decreases what in the world is visible to the camera. When FOV
16
+ # is 60 degrees then ~1.2 by ~1.2 world view is visible through z=-1.
17
+ #
18
+ # The visible world view is then split into pixels bsaed on the l / w of the
19
+ # desired screen. The pixel size is calculated using these l / w dimensions.
20
+ # The pixels are defined to be evenly spaced within the visible world.
21
+ #
22
+ # An example of a default 90 degree FOV and w=5 / h=4 results in pixels that
23
+ # are of size 0.4 (the greater of 2.0 / w=5 and 2.0 / h=4). With these
24
+ # dimensions rays are cast to the center of pixels evenly distrubted across
25
+ # the screen.
26
+ class Camera
27
+ attr_accessor :h, :w, :fov, :transform
28
+
29
+ DEFAULT_W = 1280
30
+ DEFAULT_H = 1024
31
+ DEFAULT_FOV = Math::PI / 3
32
+
33
+ # @param transform [Ra::Transform]
34
+ # @param h [Numeric]
35
+ # @param w [Numeric]
36
+ # @param fov [Numeric]
37
+ def initialize(transform: Transform::IDENTITY, h: DEFAULT_H, w: DEFAULT_W, fov: DEFAULT_FOV)
38
+ @transform = transform
39
+ @h = h
40
+ @w = w
41
+ @fov = fov
42
+ end
43
+
44
+ # @param x [Numeric]
45
+ # @param y [Numeric]
46
+ # @return [Ra::Ray]
47
+ def ray(x:, y:)
48
+ pixel = transform.inverse * Vector[world_x(x:), world_y(y:), -1, Tuple::POINT]
49
+ origin = transform.inverse * Vector[0, 0, 0, Tuple::POINT]
50
+
51
+ direction = (pixel - origin).normalize
52
+
53
+ Ray.new(origin:, direction:)
54
+ end
55
+
56
+ # @return [Float]
57
+ def p_size
58
+ @p_size ||= half_w * 2 / w
59
+ end
60
+
61
+ # @return [Float]
62
+ def half_view
63
+ @half_view ||= Math.tan(@fov / 2)
64
+ end
65
+
66
+ # @return [Float]
67
+ def half_w
68
+ @half_w ||= @w < @h ? (half_view * @w / @h) : half_view
69
+ end
70
+
71
+ # @return [Float]
72
+ def half_h
73
+ @half_h ||= @h < @w ? (half_view * @h / @w) : half_view
74
+ end
75
+
76
+ private
77
+
78
+ # @param y [Float]
79
+ # @return [Float]
80
+ def world_y(y:)
81
+ offset_y = (y + 0.5) * p_size
82
+ half_h - offset_y
83
+ end
84
+
85
+ # @param x [Float]
86
+ # @return [Float]
87
+ def world_x(x:)
88
+ offset_x = (x + 0.5) * p_size
89
+ half_w - offset_x
90
+ end
91
+ end
92
+ end
data/lib/ra/canvas.rb ADDED
@@ -0,0 +1,71 @@
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
data/lib/ra/color.rb ADDED
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # Color can be represented using r / g / b. Each component is assigned a
5
+ # number between 0.0 and 1.0. A color can also be converted for use when
6
+ # saving as [PPM](https://netpbm.sourceforge.net/doc/ppm.html).
7
+ #
8
+ # color = Ra::Color.hex("#00FF00")
9
+ # color.r == 0.0
10
+ # color.g == 1.0
11
+ # color.b == 0.0
12
+ #
13
+ # color = Ra::Color.new(
14
+ # r: 0.5,
15
+ # g: 0.7,
16
+ # b: 0.9,
17
+ # )
18
+ # color.ppm == "128 179 230"
19
+ class Color
20
+ attr_accessor :r, :g, :b
21
+
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
33
+
34
+ # @param value [String] e.g. "#336699"
35
+ # @return [Ra::Color]
36
+ def self.hex(value)
37
+ r, g, b = value.match(/^#(..)(..)(..)$/).captures.map(&:hex)
38
+
39
+ new(
40
+ r: Float(r) / PRECISION,
41
+ g: Float(g) / PRECISION,
42
+ b: Float(b) / PRECISION,
43
+ )
44
+ end
45
+
46
+ # @param value [Numeric] between 0.0 and 1.0
47
+ # @return [Ra::Color]
48
+ def self.uniform(value)
49
+ new(r: value, g: value, b: value)
50
+ end
51
+
52
+ # @return [Ra::Color]
53
+ def self.white
54
+ @white ||= uniform(1.0)
55
+ end
56
+
57
+ # @return [Ra::Color]
58
+ def self.black
59
+ @black ||= uniform(0.0)
60
+ end
61
+
62
+ # @param r [Numeric] between 0.0 and 1.0
63
+ # @param g [Numeric] between 0.0 and 1.0
64
+ # @param b [Numeric] between 0.0 and 1.0
65
+ def initialize(r: 0.0, g: 0.0, b: 0.0)
66
+ @r = r
67
+ @g = g
68
+ @b = b
69
+ end
70
+
71
+ # @param precision [Integer]
72
+ # @return [Integer]
73
+ def ppm(precision: PRECISION)
74
+ "#{r_val(precision:)} #{g_val(precision:)} #{b_val(precision:)}"
75
+ end
76
+
77
+ # Combine the r / g / b components (+). If `other` is `nil` return `self`.
78
+ #
79
+ # @param other [Ra::Color, nil]
80
+ # @return [Ra::Color]
81
+ def +(other)
82
+ return self if other.nil?
83
+
84
+ self.class.new(r: r + other.r, g: g + other.g, b: b + other.b)
85
+ end
86
+
87
+ # Combine the r / g / b components (-). If `other` is `nil` return `self`.
88
+ #
89
+ # @param other [Ra::Color, nil]
90
+ # @return [Ra::Color]
91
+ def -(other)
92
+ return self if other.nil?
93
+
94
+ self.class.new(r: r - other.r, g: g - other.g, b: b - other.b)
95
+ end
96
+
97
+ # @param other [Ra::Color, Numeric]
98
+ # @return [Ra::Color]
99
+ def *(other)
100
+ is_color = other.is_a?(self.class)
101
+ other_r = is_color ? other.r : other
102
+ other_g = is_color ? other.g : other
103
+ other_b = is_color ? other.b : other
104
+
105
+ self.class.new(
106
+ r: r * other_r,
107
+ g: g * other_g,
108
+ b: b * other_b,
109
+ )
110
+ end
111
+
112
+ # @param other [Ra::Color, Numeric]
113
+ # @return [Ra::Color]
114
+ def /(other)
115
+ is_color = other.is_a?(self.class)
116
+ other_r = is_color ? other.r : other
117
+ other_g = is_color ? other.g : other
118
+ other_b = is_color ? other.b : other
119
+
120
+ self.class.new(
121
+ r: r / other_r,
122
+ g: g / other_g,
123
+ b: b / other_b,
124
+ )
125
+ end
126
+
127
+ # @return [Boolean]
128
+ def ==(other)
129
+ r_val == other.r_val && g_val == other.g_val && b_val == other.b_val
130
+ end
131
+
132
+ protected
133
+
134
+ # @return [Integer]
135
+ def r_val(precision: PRECISION)
136
+ val(value: r, precision:)
137
+ end
138
+
139
+ # @return [Integer]
140
+ def g_val(precision: PRECISION)
141
+ val(value: g, precision:)
142
+ end
143
+
144
+ # @return [Integer]
145
+ def b_val(precision: PRECISION)
146
+ val(value: b, precision:)
147
+ end
148
+
149
+ private
150
+
151
+ # @param value [Numeric]
152
+ # @param precision [Integer]
153
+ # @return [Integer]
154
+ def val(value:, precision: PRECISION)
155
+ (value * precision).clamp(0, precision).round
156
+ end
157
+ end
158
+ end
data/lib/ra/engine.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # An engine takes uses a world / camera to generate a canvas.
5
+ class Engine
6
+ PRECISION = 255
7
+
8
+ # @param world [Ra::World]
9
+ # @param camera [Ra::Camera]
10
+ def initialize(world:, camera:)
11
+ @world = world
12
+ @camera = camera
13
+ end
14
+
15
+ # @return [Ra::Canvas]
16
+ def render
17
+ Ra::Canvas.new(w: @camera.w, h: @camera.h, precision: PRECISION).tap do |canvas|
18
+ @camera.h.times do |y|
19
+ @camera.w.times do |x|
20
+ draw(x:, y:, canvas:)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @param x [Integer]
29
+ # @param y [Integer]
30
+ # @param canvas [Ra::Canvas]
31
+ def draw(x:, y:, canvas:)
32
+ ray = @camera.ray(x:, y:)
33
+
34
+ intersections = @world.intersect(ray:)
35
+ intersection = Intersection.hit(intersections:)
36
+
37
+ canvas[x, y] = @world.color(intersection:) if intersection
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # An intersection tracks the time t at which a shape is intersected by a ray.
5
+ class Intersection
6
+ attr_accessor :t, :ray, :shape
7
+
8
+ # @param intersections Array<Ra::Intersection>
9
+ # @return [Ra::Intersection, nil]
10
+ def self.hit(intersections:)
11
+ hit = nil
12
+
13
+ intersections.each do |interaction|
14
+ next if interaction.t.negative?
15
+
16
+ hit = interaction if hit.nil? || interaction.t < hit.t
17
+ end
18
+
19
+ hit
20
+ end
21
+
22
+ # @param t [Numeric]
23
+ # @param ray [Ra::Ray]
24
+ # @param shape [Ra::Shape::Base]
25
+ def initialize(t:, ray:, shape:)
26
+ @t = t
27
+ @ray = ray
28
+ @shape = shape
29
+ end
30
+
31
+ # @return [Boolean]
32
+ def ==(other)
33
+ t == other.t && ray == other.ray && shape == other.shape
34
+ end
35
+
36
+ # @return [Ra::Surface]
37
+ def surface
38
+ point = ray.position(t:)
39
+ eyev = -ray.direction
40
+ normalv = shape.normal(point:)
41
+
42
+ Surface.new(shape:, eyev:, normalv:, point:)
43
+ end
44
+ end
45
+ end
data/lib/ra/light.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A light has a position within a scene and an intensity it is rendered
5
+ # with. For example:
6
+ #
7
+ # light = Ra::Light.new(
8
+ # intensity: Ra::Color.uniform(0.8),
9
+ # position: Vector[0, 0, 0, Ra::Tuple::POINT],
10
+ # )
11
+ class Light
12
+ attr_accessor :intensity, :position
13
+
14
+ # @param intensity [Ra::Color]
15
+ # @param position [Vector]
16
+ def initialize(intensity:, position:)
17
+ @intensity = intensity
18
+ @position = position
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # Lighting encaspulates a [Phong Reflection Model](https://en.wikipedia.org/wiki/phong_reflection_model).
5
+ class Lighting
6
+ attr_accessor :light, :surface, :shadowed
7
+
8
+ # @param light [Ra::Light]
9
+ # @param surface [Ra::Surface]
10
+ # @param shadowed [Boolean]
11
+ def initialize(light:, surface:, shadowed:)
12
+ @surface = surface
13
+ @shadowed = shadowed
14
+ @light = light
15
+ end
16
+
17
+ # @return [Ra::Color]
18
+ def color
19
+ ambient_color + diffuse_color + specular_color
20
+ end
21
+
22
+ private
23
+
24
+ # @return [Ra::Shape]
25
+ def shape
26
+ surface.shape
27
+ end
28
+
29
+ # @return [Vector]
30
+ def point
31
+ surface.point
32
+ end
33
+
34
+ # @return [Vector]
35
+ def normalv
36
+ surface.normalv
37
+ end
38
+
39
+ # @return [Vector]
40
+ def eyev
41
+ surface.eyev
42
+ end
43
+
44
+ # @return [Ra::Material]
45
+ def material
46
+ shape.material
47
+ end
48
+
49
+ # @return [Ra::Vector]
50
+ def lightv
51
+ @lightv ||= (light.position - point).normalize
52
+ end
53
+
54
+ # @return [Ra::Vector]
55
+ def reflectv
56
+ @reflectv ||= -(lightv - (normalv * 2 * lightv.dot(normalv)))
57
+ end
58
+
59
+ # @return [Ra::Vector]
60
+ def light_dot_normal
61
+ @light_dot_normal ||= lightv.dot(normalv)
62
+ end
63
+
64
+ # @return [Ra::Vector]
65
+ def reflect_dot_eye
66
+ @reflect_dot_eye ||= reflectv.dot(eyev)
67
+ end
68
+
69
+ # @return [Ra::Color]
70
+ def ambient_color
71
+ @ambient_color ||= effective_color * material.ambient
72
+ end
73
+
74
+ # @return [Ra::Color, nil]
75
+ def diffuse_color
76
+ return if shadowed
77
+ return if light_dot_normal.negative?
78
+
79
+ @diffuse_color ||= effective_color * material.diffuse * light_dot_normal
80
+ end
81
+
82
+ # @return [Ra::Color, nil]
83
+ def specular_color
84
+ return if shadowed
85
+ return if light_dot_normal.negative?
86
+ return unless reflect_dot_eye.positive?
87
+
88
+ @specular_color ||= light.intensity * material.specular * (reflect_dot_eye**material.shininess)
89
+ end
90
+
91
+ # @return [Ra::Color]
92
+ def effective_color
93
+ @effective_color ||= shape.color(point:) * light.intensity
94
+ end
95
+ end
96
+ end