ra 0.1.0 → 0.2.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.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A transform is represnted with a four by four matrix. These transforms are useful manupulate vectors. For example:
5
+ #
6
+ # p = Vector[1, 2, 3, Ra::Tuple::POINT]
7
+ # Ra::Transform.translate(2, 3, 4) * p == Vector[3, 5, 7, Ra::Tupl::POINT]
8
+ # Ra::Transform.scale(1, 2, 3) * p == Vector[1, 4, 9, Ra::Tuple::POINT]
9
+ #
10
+ # As a convenience, transforms can also be chained:
11
+ #
12
+ # Ra::Transform
13
+ # .translate(2, 3, 4)
14
+ # .scale(1, 2, 3)
15
+ # .rotate_x(Math::PI / 4)
16
+ # .rotate_y(Math::PI / 4)
17
+ # .rotate_z(Math::PI / 4)
18
+ class Transform < ::Matrix
19
+ IDENTITY = identity(4)
20
+
21
+ # @param from [Vector]
22
+ # @param to [Vector]
23
+ # @param up [Vector]
24
+ # @return [Ra::Transform]
25
+ def self.view(from:, to:, up:)
26
+ f = (to - from).normalize
27
+ l = Tuple.cross(f, up.normalize)
28
+ u = Tuple.cross(l, f)
29
+
30
+ self[l, u, -f, [0, 0, 0, 1]]
31
+ .translate(-from[0], -from[1], -from[2])
32
+ end
33
+
34
+ # @return [Ra::Transform]
35
+ # @param x [Numeric]
36
+ # @param y [Numeric]
37
+ # @param z [Numeric]
38
+ def self.translate(x, y, z)
39
+ self[
40
+ [1, 0, 0, x],
41
+ [0, 1, 0, y],
42
+ [0, 0, 1, z],
43
+ [0, 0, 0, 1],
44
+ ]
45
+ end
46
+
47
+ # @return [Ra::Transform]
48
+ # @param x [Numeric]
49
+ # @param y [Numeric]
50
+ # @param z [Numeric]
51
+ def self.scale(x, y, z)
52
+ self[
53
+ [x, 0, 0, 0],
54
+ [0, y, 0, 0],
55
+ [0, 0, z, 0],
56
+ [0, 0, 0, 1],
57
+ ]
58
+ end
59
+
60
+ # @return [Ra::Transform]
61
+ # @param rotation [Numeric]
62
+ def self.rotate_x(rotation)
63
+ self[
64
+ [1, 0, 0, 0],
65
+ [0, +Math.cos(rotation), -Math.sin(rotation), 0],
66
+ [0, +Math.sin(rotation), +Math.cos(rotation), 0],
67
+ [0, 0, 0, 1],
68
+ ]
69
+ end
70
+
71
+ # @return [Ra::Transform]
72
+ # @param rotation [Numeric]
73
+ def self.rotate_y(rotation)
74
+ self[
75
+ [+Math.cos(rotation), 0, +Math.sin(rotation), 0],
76
+ [0, 1, 0, 0],
77
+ [-Math.sin(rotation), 0, +Math.cos(rotation), 0],
78
+ [0, 0, 0, 1],
79
+ ]
80
+ end
81
+
82
+ # @return [Ra::Transform]
83
+ # @param rotation [Numeric]
84
+ def self.rotate_z(rotation)
85
+ self[
86
+ [+Math.cos(rotation), -Math.sin(rotation), 0, 0],
87
+ [+Math.sin(rotation), +Math.cos(rotation), 0, 0],
88
+ [0, 0, 1, 0],
89
+ [0, 0, 0, 1],
90
+ ]
91
+ end
92
+
93
+ # @return [Ra::Transform]
94
+ # @param x [Numeric]
95
+ # @param y [Numeric]
96
+ # @param z [Numeric]
97
+ def translate(...)
98
+ self * self.class.translate(...)
99
+ end
100
+
101
+ # @return [Ra::Transform]
102
+ # @param x [Numeric]
103
+ # @param y [Numeric]
104
+ # @param z [Numeric]
105
+ def scale(...)
106
+ self * self.class.scale(...)
107
+ end
108
+
109
+ # @return [Ra::Transform]
110
+ # @param rotation [Numeric]
111
+ def rotate_x(...)
112
+ self * self.class.rotate_x(...)
113
+ end
114
+
115
+ # @return [Ra::Transform]
116
+ # @param rotation [Numeric]
117
+ def rotate_y(...)
118
+ self * self.class.rotate_y(...)
119
+ end
120
+
121
+ # @return [Ra::Transform]
122
+ # @param rotation [Numeric]
123
+ def rotate_z(...)
124
+ self * self.class.rotate_z(...)
125
+ end
126
+
127
+ # Avoid re-computing a transform inverse by memoizing.
128
+ #
129
+ # @return [Ra::Transform]
130
+ def inverse
131
+ @inverse ||= super
132
+ end
133
+
134
+ # Avoid re-computing a transform tranpose by memoizing.
135
+ #
136
+ # @return [Ra::Transform]
137
+ def tranpose
138
+ @tranpose ||= super
139
+ end
140
+ end
141
+ end
data/lib/ra/tuple.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A Tuple can classify a Vector as a POINT or VECTOR. For example:
5
+ #
6
+ # point = Vector[x = 1, y = 2, z = 3, w = Ra::Tuple::POINT]
7
+ # vector = Vector[x = 1, y = 2, z = 3, w = Ra::Tuple::VECTOR]
8
+ module Tuple
9
+ POINT = 1
10
+ VECTOR = 0
11
+
12
+ # @return source [Vector]
13
+ # @return target [Vector]
14
+ # @return [Vector]
15
+ def self.cross(source, target)
16
+ cross = Vector[source[0], source[1], source[2]].cross(Vector[target[0], target[1], target[2]])
17
+
18
+ Vector[
19
+ cross[0],
20
+ cross[1],
21
+ cross[2],
22
+ Tuple::VECTOR,
23
+ ]
24
+ end
25
+ end
26
+ end
data/lib/ra/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ra
4
- VERSION = "0.1.0"
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/ra/world.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ra
4
+ # A world is composed of objects (lights / cameras) and handles coloring of rays.
5
+ class World
6
+ attr_accessor :light, :shapes
7
+
8
+ # @param light [Ra::Light]
9
+ # @param shapes [Array<Ra::Shape>]
10
+ def initialize(light:, shapes:)
11
+ @light = light
12
+ @shapes = shapes
13
+ end
14
+
15
+ # @param ray [Ra::Ray]
16
+ # @return [Array<Ra::Intersection>]
17
+ def intersect(ray:)
18
+ @shapes
19
+ .map { |shape| shape.intersect(ray:) }
20
+ .reduce(:concat) || []
21
+ end
22
+
23
+ # @return [Ra::Intersection, nil]
24
+ def intersection(ray:)
25
+ intersections = intersect(ray:)
26
+ Intersection.hit(intersections:)
27
+ end
28
+
29
+ # @param intersection [Ra::Intersection]
30
+ # @return [Ra::Color]
31
+ def color(intersection:)
32
+ surface = intersection.surface
33
+ shadowed = shadowed?(point: surface.hpoint)
34
+
35
+ Lighting.new(shadowed:, surface:, light:).color
36
+ end
37
+
38
+ # @param point [Ra::Point]
39
+ def shadowed?(point:)
40
+ vector = @light.position - point
41
+ distance = vector.magnitude
42
+ direction = vector.normalize
43
+ ray = Ray.new(origin: point, direction:)
44
+ hit = intersection(ray:)
45
+ hit && hit.t < distance
46
+ end
47
+ end
48
+ end
data/lib/ra.rb CHANGED
@@ -1,8 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ra/version"
3
+ require 'matrix'
4
+ require 'zeitwerk'
4
5
 
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ # Named for ["Ra"](https://en.wikipedia.org/wiki/Ra).
5
10
  module Ra
6
- class Error < StandardError; end
7
- # Your code goes here...
11
+ EPSILON = (2**-16)
12
+
13
+ # @return [Ra::Logger]
14
+ def self.logger
15
+ @logger ||= Logger.new
16
+ end
8
17
  end
metadata CHANGED
@@ -1,33 +1,97 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ra
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.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-20 00:00:00.000000000 Z
12
- dependencies: []
13
- description: "..."
11
+ date: 2023-11-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: matrix
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
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'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: This should probably never be used for anything but learning.
14
56
  email:
15
57
  - kevin@ksylvest.com
16
- executables: []
58
+ executables:
59
+ - ra
17
60
  extensions: []
18
61
  extra_rdoc_files: []
19
62
  files:
20
- - ".rspec"
21
- - ".rubocop.yml"
22
63
  - README.md
23
- - Rakefile
64
+ - exe/ra
24
65
  - lib/ra.rb
66
+ - lib/ra/camera.rb
67
+ - lib/ra/canvas.rb
68
+ - lib/ra/color.rb
69
+ - lib/ra/engine.rb
70
+ - lib/ra/intersection.rb
71
+ - lib/ra/light.rb
72
+ - lib/ra/lighting.rb
73
+ - lib/ra/logger.rb
74
+ - lib/ra/material.rb
75
+ - lib/ra/pattern/base.rb
76
+ - lib/ra/pattern/checkers.rb
77
+ - lib/ra/pattern/gradient.rb
78
+ - lib/ra/pattern/rings.rb
79
+ - lib/ra/pattern/stripes.rb
80
+ - lib/ra/ray.rb
81
+ - lib/ra/shape/base.rb
82
+ - lib/ra/shape/cube.rb
83
+ - lib/ra/shape/plane.rb
84
+ - lib/ra/shape/sphere.rb
85
+ - lib/ra/surface.rb
86
+ - lib/ra/transform.rb
87
+ - lib/ra/tuple.rb
25
88
  - lib/ra/version.rb
26
- - sig/ra.rbs
89
+ - lib/ra/world.rb
27
90
  homepage: https://github.com/ksylvest/ra
28
- licenses: []
91
+ licenses:
92
+ - MIT
29
93
  metadata:
30
- homepage_uri: https://github.com/ksylvest/ra
94
+ rubygems_mfa_required: 'true'
31
95
  post_install_message:
32
96
  rdoc_options: []
33
97
  require_paths:
@@ -36,7 +100,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
36
100
  requirements:
37
101
  - - ">="
38
102
  - !ruby/object:Gem::Version
39
- version: '0'
103
+ version: '3.2'
40
104
  required_rubygems_version: !ruby/object:Gem::Requirement
41
105
  requirements:
42
106
  - - ">="
@@ -46,5 +110,5 @@ requirements: []
46
110
  rubygems_version: 3.4.22
47
111
  signing_key:
48
112
  specification_version: 4
49
- summary: "..."
113
+ summary: A graphics library written for fun.
50
114
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,13 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6
3
-
4
- Style/StringLiterals:
5
- Enabled: true
6
- EnforcedStyle: double_quotes
7
-
8
- Style/StringLiteralsInInterpolation:
9
- Enabled: true
10
- EnforcedStyle: double_quotes
11
-
12
- Layout/LineLength:
13
- Max: 120
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require "rubocop/rake_task"
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]
data/sig/ra.rbs DELETED
@@ -1,4 +0,0 @@
1
- module Ra
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end