├── lib ├── geo3d │ ├── version.rb │ ├── utils.rb │ ├── triangle.rb │ ├── plane.rb │ ├── vector.rb │ ├── quaternion.rb │ └── matrix.rb └── geo3d.rb ├── spec ├── spec_helper.rb ├── lib │ ├── triangle_spec.rb │ ├── plane_spec.rb │ ├── vector_spec.rb │ ├── quaternion_spec.rb │ └── matrix_spec.rb └── opengl │ └── matrix_spec.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── geo3d.gemspec ├── LICENSE.txt └── README.md /lib/geo3d/version.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | VERSION = "0.1.7" 3 | end 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'geo3d' 2 | 3 | RSpec.configure do |config| 4 | config.expect_with(:rspec) { |c| c.syntax = :should } 5 | end 6 | 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | 9 | -------------------------------------------------------------------------------- /lib/geo3d.rb: -------------------------------------------------------------------------------- 1 | require 'geo3d/version' 2 | require 'geo3d/utils' 3 | require 'geo3d/vector' 4 | require 'geo3d/matrix' 5 | require 'geo3d/quaternion' 6 | require 'geo3d/plane' 7 | require 'geo3d/triangle' 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in geo3d.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'ruby-opengl', '~> 0.61.0' 8 | gem 'glu', '~> 8.3.0' 9 | gem 'glut', '~> 8.3.0' 10 | end 11 | -------------------------------------------------------------------------------- /lib/geo3d/utils.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | module Utils 3 | def self.float_cmp a, b, tolerance = 0.01 4 | (a-b).abs < tolerance 5 | end 6 | 7 | def self.to_degrees radians 8 | radians * 180.0 / Math::PI 9 | end 10 | 11 | def self.to_radians degrees 12 | degrees * Math::PI / 180.0 13 | end 14 | 15 | def self.normalize_angle radians 16 | if radians.abs > Math::PI * 2.0 17 | absolute = radians.abs % (Math::PI * 2.0 ) 18 | if radians < 0 19 | -absolute 20 | else 21 | absolute 22 | end 23 | else 24 | radians 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /spec/lib/triangle_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Geo3d::Triangle do 4 | it "should detect winding" do 5 | [{:a => [0,0,0], :b => [1,0,0], :c => [1,1,0], :expected => "counter-clockwise"}, 6 | {:a => [0,0,0], :b => [1,0,0], :c => [1,-1,0], :expected => "clockwise"}].each do |data| 7 | t = Geo3d::Triangle.new 8 | t.a = Geo3d::Vector.new *data[:a] 9 | t.b = Geo3d::Vector.new *data[:b] 10 | t.c = Geo3d::Vector.new *data[:c] 11 | winding = t.counter_clockwise?? 'counter-clockwise' : 'clockwise' 12 | winding.should == data[:expected] 13 | end 14 | end 15 | 16 | it "should calculate normal" do 17 | 18 | end 19 | end -------------------------------------------------------------------------------- /geo3d.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'geo3d/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "geo3d" 8 | spec.version = Geo3d::VERSION 9 | spec.authors = ["Misha Conway"] 10 | spec.email = ["MishaAConway@gmail.com"] 11 | spec.description = %q{Library for common 3d graphics vector and matrix operations} 12 | spec.summary = %q{Library for common 3d graphics vector and matrix operations} 13 | spec.homepage = "https://github.com/MishaConway/geo3d" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.3" 22 | spec.add_development_dependency "rake" 23 | spec.add_development_dependency "rspec" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Misha Conway 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/geo3d/triangle.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | class Triangle 3 | attr_accessor :a, :b, :c 4 | 5 | def points 6 | [a, b, c] 7 | end 8 | 9 | def initialize *args 10 | @a = args.size > 0 ? args[0] : Vector.new 11 | @b = args.size > 1 ? args[1] : Vector.new 12 | @c = args.size > 2 ? args[2] : Vector.new 13 | end 14 | 15 | def flip! 16 | @b, @c = @c, @b 17 | end 18 | 19 | def flip 20 | f = clone 21 | f.flip! 22 | f 23 | end 24 | 25 | def normal 26 | (b - a).cross(c - a).normalize 27 | end 28 | 29 | def signed_area reference_normal = Vector.new(0,0,-1) 30 | sum = Vector.new 0, 0, 0, 0 31 | points.each_with_index do |current_point, i| 32 | next_point = points[(i == points.size - 1) ? 0 : i+1] 33 | sum += current_point.cross next_point 34 | end 35 | reference_normal.dot(sum) / 2.0 36 | end 37 | 38 | def clockwise? reference_normal = Vector.new(0,0,-1) 39 | signed_area( reference_normal ) > 0 40 | end 41 | 42 | def counter_clockwise? reference_normal = Vector.new(0,0,-1) 43 | signed_area( reference_normal ) < 0 44 | end 45 | 46 | alias :cw? :clockwise? 47 | alias :ccw? :counter_clockwise? 48 | end 49 | end -------------------------------------------------------------------------------- /lib/geo3d/plane.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | class Plane 3 | attr_accessor :a, :b, :c, :d 4 | alias :x :a 5 | alias :y :b 6 | alias :z :c 7 | alias :w :d 8 | 9 | def initialize *args 10 | @a, @b, @c, @d = 0.0, 0.0, 0.0, 0.0 11 | @a = args[0].to_f if args.size > 0 12 | @b = args[1].to_f if args.size > 1 13 | @c = args[2].to_f if args.size > 2 14 | @d = args[3].to_f if args.size > 3 15 | end 16 | 17 | def self.from_points pv1, pv2, pv3 18 | edge1 = pv2 - pv1 19 | edge2 = pv3 - pv1 20 | from_point_and_normal pv1, edge1.cross(edge2).normalize 21 | end 22 | 23 | def self.from_point_and_normal point, normal 24 | point.w = 0 25 | self.new normal.x, normal.y, normal.z, -point.dot(normal) 26 | end 27 | 28 | def to_a 29 | [a,b,c,d] 30 | end 31 | 32 | def == p 33 | Geo3d::Utils.float_cmp(a, p.a) && Geo3d::Utils.float_cmp(b, p.b) && Geo3d::Utils.float_cmp(c, p.c) && Geo3d::Utils.float_cmp(d, p.d) 34 | end 35 | 36 | def != vec 37 | !(self == vec) 38 | end 39 | 40 | def dot v 41 | a * v.x + b * v.y + c * v.z + d * v.w 42 | end 43 | 44 | def normalize! 45 | norm = Math.sqrt(a*a + b*b + c*c) 46 | if norm.zero? 47 | @a = 0 48 | @b = 0 49 | @c = 0 50 | @d = 0 51 | else 52 | @a /= norm 53 | @b /= norm 54 | @c /= norm 55 | @d /= norm 56 | end 57 | end 58 | 59 | def normalize 60 | p = self.class.new a, b, c, d 61 | p.normalize! 62 | p 63 | end 64 | 65 | def normal 66 | Vector.new a, b, c 67 | end 68 | 69 | def line_intersection line_start, line_end 70 | direction = line_end - line_start 71 | 72 | normal_dot_direction = normal.dot direction 73 | 74 | if (normal_dot_direction.zero?) 75 | nil 76 | else 77 | temp = (d + normal.dot(line_start)) / normal_dot_direction 78 | line_start - direction * temp 79 | end 80 | end 81 | 82 | def transform matrix, use_inverse_transpose = true 83 | matrix = matrix.inverse.transpose if use_inverse_transpose 84 | p = self.class.new 85 | p.a = dot matrix.row(0) 86 | p.b = dot matrix.row(1) 87 | p.c = dot matrix.row(2) 88 | p.d = dot matrix.row(3) 89 | p 90 | end 91 | end 92 | end -------------------------------------------------------------------------------- /lib/geo3d/vector.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | class Vector 3 | attr_accessor :x, :y, :z, :w 4 | alias :a :x 5 | alias :b :y 6 | alias :c :z 7 | alias :d :w 8 | 9 | def initialize *args 10 | @x, @y, @z, @w = 0.0, 0.0, 0.0, 0.0 11 | @x = args[0].to_f if args.size > 0 12 | @y = args[1].to_f if args.size > 1 13 | @z = args[2].to_f if args.size > 2 14 | @w = args[3].to_f if args.size > 3 15 | end 16 | 17 | def self.point *args 18 | self.new(*args).one_w 19 | end 20 | 21 | def self.direction *args 22 | self.new(*args).zero_w 23 | end 24 | 25 | def zero_w 26 | self.class.new x, y, z, 0 27 | end 28 | 29 | def one_w 30 | self.class.new x, y, z, 1 31 | end 32 | 33 | def xyz 34 | self.class.new x, y, z, 0 35 | end 36 | 37 | def to_s 38 | to_a.compact.join ' ' 39 | end 40 | 41 | def to_a 42 | [x, y, z, w] 43 | end 44 | 45 | def +@ 46 | self * 1 47 | end 48 | 49 | def -@ 50 | self * -1 51 | end 52 | 53 | def + vec 54 | self.class.new x + vec.x, y + vec.y, z + vec.z, w 55 | end 56 | 57 | def - vec 58 | self.class.new x - vec.x, y - vec.y, z - vec.z, w 59 | end 60 | 61 | def * scalar 62 | self.class.new x * scalar, y * scalar, z * scalar, w 63 | end 64 | 65 | def / scalar 66 | self.class.new x / scalar, y / scalar, z / scalar, w 67 | end 68 | 69 | def == vec 70 | Geo3d::Utils.float_cmp(x, vec.x) && 71 | Geo3d::Utils.float_cmp(y, vec.y) && 72 | Geo3d::Utils.float_cmp(z, vec.z) && 73 | Geo3d::Utils.float_cmp(w, vec.w) 74 | end 75 | 76 | def != vec 77 | !(self == vec) 78 | end 79 | 80 | def cross vec 81 | self.class.new y * vec.z - z * vec.y, z * vec.x - x * vec.z, x * vec.y - y * vec.x 82 | end 83 | 84 | def dot vec 85 | x * vec.x + y * vec.y + z * vec.z + w * vec.w 86 | end 87 | 88 | def normalize! 89 | len = length 90 | if length > 0 91 | @x /= len 92 | @y /= len 93 | @z /= len 94 | @w /= len 95 | end 96 | end 97 | 98 | def normalize 99 | v = self.class.new x, y, z, w 100 | v.normalize! 101 | v 102 | end 103 | 104 | def length 105 | Math.sqrt length_squared 106 | end 107 | 108 | def length_squared 109 | dot self 110 | end 111 | 112 | def lerp vec, s 113 | l = self + (vec - self)*s 114 | l.w = w + (vec.w - w)*s 115 | l 116 | end 117 | 118 | def project viewport, projection, view, world 119 | clipspace_vector = projection * view * world * one_w 120 | normalized_clipspace_vector = (clipspace_vector / clipspace_vector.w.to_f).one_w 121 | viewport * normalized_clipspace_vector 122 | end 123 | 124 | def unproject viewport, projection, view, world 125 | normalized_clipspace_vector = viewport.inverse * one_w 126 | almost_objectspace_vector = (projection * view * world).inverse * normalized_clipspace_vector.one_w 127 | (almost_objectspace_vector / almost_objectspace_vector.w).one_w 128 | end 129 | 130 | def self.reflect normal, incident 131 | s = 2.0 * normal.xyz.dot(incident.xyz) 132 | (incident - normal * s).xyz 133 | end 134 | 135 | def self.refract normal, incident, index_of_refraction 136 | t = incident.xyz.dot normal.xyz 137 | r = 1.0 - index_of_refraction * index_of_refraction * (1.0 - t*t) 138 | 139 | if r < 0.0 # Total internal reflection 140 | self.new 0, 0, 0, 0 141 | else 142 | s = index_of_refraction * t + Math.sqrt(r) 143 | (incident * index_of_refraction - normal * s).xyz 144 | end 145 | end 146 | end 147 | end 148 | 149 | -------------------------------------------------------------------------------- /spec/lib/plane_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Geo3d::Plane do 4 | it "should default all values to zero" do 5 | p = Geo3d::Plane.new 6 | p.a.zero?.should == true 7 | p.b.zero?.should == true 8 | p.c.zero?.should == true 9 | p.d.zero?.should == true 10 | end 11 | 12 | it "should construct from a point and a normal" do 13 | [{:point => [0, 0, 0], :normal => [0, 1, 0], :expected => [0, 1, 0, 0]}, 14 | {:point => [1, 2, 3], :normal => [1, 1, 1], :expected => [1, 1, 1, -6]}, 15 | {:point => [221, 772, 33], :normal => [2, 0, 1], :expected => [2, 0, 1, -475]}, 16 | {:point => [999, 888, 777], :normal => [-1, 3, 0], :expected => [-1, 3, 0, -1665]}].each do |data| 17 | plane = Geo3d::Plane.from_point_and_normal Geo3d::Vector.new(*data[:point]), Geo3d::Vector.new(*data[:normal]) 18 | expected = Geo3d::Plane.new *data[:expected] 19 | plane.should == expected 20 | end 21 | end 22 | 23 | it "should construct from points" do 24 | [{:a => [1, 1, 1], :b => [2, 2, 2], :c => [3, 3, 3], :expected => [0, 0, 0, 0]}, 25 | {:a => [-1.5, 1.5, 3.0, 0], :b => [1.5, 1.5, 3.0, 0], :c => [-1.5, -1.5, 3.0, 0], :expected => [0, 0, -1, 3]}, 26 | {:a => [10, 1, 33], :b => [1, 11, 3], :c => [-1, -1, 3], :expected => [-0.930808, 0.155135, 0.330954, -1.768534]}].each do |data| 27 | plane = Geo3d::Plane.from_points Geo3d::Vector.new(*data[:a]), Geo3d::Vector.new(*data[:b]), Geo3d::Vector.new(*data[:c]) 28 | expected = Geo3d::Plane.new *data[:expected] 29 | plane.should == expected 30 | end 31 | end 32 | 33 | it "should support dot products with vectors" do 34 | [{:plane => [1, 1, 1, 1], :vector => [2, 2, 2, 2], :expected => 8}, 35 | {:plane => [0, 1, 0, -99], :vector => [0, -42, 2, 52], :expected => -5190}, 36 | {:plane => [91, -2731, 1, 123], :vector => [2, 7, -9, 2], :expected => -18698}, 37 | ].each do |data| 38 | plane = Geo3d::Plane.new *data[:plane] 39 | vector = Geo3d::Vector.new *data[:vector] 40 | Geo3d::Utils.float_cmp(plane.dot(vector), data[:expected]).should == true 41 | end 42 | end 43 | 44 | it "should be normalizable" do 45 | [{:plane => [0, 0, -1, 3], :expected => [0, 0, -1, 3]}, 46 | {:plane => [11, 11, -7, 3], :expected => [0.644831, 0.644831, -0.410347, 0.175863]}, 47 | {:plane => [-56, 23, 923, 9], :expected => [-0.060542, 0.024865, 0.997856, 0.009730]}].each do |data| 48 | plane = Geo3d::Plane.new(*data[:plane]).normalize 49 | expected = Geo3d::Plane.new *data[:expected] 50 | plane.should == expected 51 | end 52 | end 53 | 54 | it "should be able to detect line intersections" do 55 | [{:plane => [0,0,-1,3], :line_start => [1,1,1,0], :line_end => [2,2,2,0], :expected => [3,3,3,0]}, 56 | {:plane => [0,0,-1,3], :line_start => [1,1,1,1], :line_end => [2,2,2,1], :expected => [3,3,3,1]}, 57 | {:plane => [0,1,0,-30], :line_start => [1,0,0,0], :line_end => [2,0,0,0], :expected => nil}, 58 | {:plane => [0,1,0,-30], :line_start => [1,1,0,0], :line_end => [2,0,0,0], :expected => [-28,30,0,0]}, 59 | {:plane => [0,1,0,-30], :line_start => [1,1,0,1], :line_end => [2,0,0,1], :expected => [-28,30,0,1]}].each do |data| 60 | plane = Geo3d::Plane.new *data[:plane] 61 | line_start = Geo3d::Vector.new *data[:line_start] 62 | line_end = Geo3d::Vector.new *data[:line_end] 63 | if data[:expected] 64 | expected = Geo3d::Vector.new *data[:expected] 65 | else 66 | expected = nil 67 | end 68 | plane.line_intersection(line_start, line_end).should == expected 69 | end 70 | end 71 | 72 | it "should be transformable" do 73 | test_transform = ->(matrix, plane,expected) do 74 | plane.transform(matrix).should == expected 75 | end 76 | test_transform.call Geo3d::Matrix.translation(0,5,0), Geo3d::Plane.new(0,1,0,0), Geo3d::Plane.new(0,1,0,-5) 77 | test_transform.call Geo3d::Matrix.translation(0,5,0), Geo3d::Plane.new(1,1,1,1), Geo3d::Plane.new(1,1,1,-4) 78 | test_transform.call Geo3d::Matrix.rotation_x(1), Geo3d::Plane.new(1,1,1,1), Geo3d::Plane.new(1.000000, -0.301169, 1.381773, 1.000000) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/vector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Geo3d::Vector do 4 | it "should default all values to zero" do 5 | v = Geo3d::Vector.new 6 | v.x.zero?.should == true 7 | v.y.zero?.should == true 8 | v.z.zero?.should == true 9 | v.w.zero?.should == true 10 | end 11 | 12 | it "should support dot products with other vectors" do 13 | [{:a => [1, 1, 1, 1], :b => [2, 2, 2, 2], :expected => 8}, 14 | {:a => [0, 1, 0, -99], :b => [0, -42, 2, 52], :expected => -5190}, 15 | {:a => [91, -2731, 1, 123], :b => [2, 7, -9, 2], :expected => -18698}, 16 | ].each do |data| 17 | a = Geo3d::Vector.new *data[:a] 18 | b = Geo3d::Vector.new *data[:b] 19 | Geo3d::Utils.float_cmp(a.dot(b), data[:expected]).should == true 20 | end 21 | end 22 | 23 | it "should support cross products with other vectors" do 24 | [{:a => [1, 0, 0, 0], :b => [0, 1, 0, 0], :expected => [0, 0, 1, 0]}, 25 | {:a => [1, 0, 0, 1], :b => [0, 1, 0, 1], :expected => [0, 0, 1, 0]}, 26 | {:a => [1, 1, 1, 1], :b => [1, 1, 1, 1], :expected => [0, 0, 0, 0]}, 27 | {:a => [2, 99, 6, 0], :b => [-11, -91, 77, 0], :expected => [8169.000000, -220.000000, 907.000000, 0.000000]}].each do |data| 28 | a = Geo3d::Vector.new *data[:a] 29 | b = Geo3d::Vector.new *data[:b] 30 | expected = Geo3d::Vector.new *data[:expected] 31 | a.cross(b).should == expected 32 | b.cross(a).should == -expected 33 | end 34 | end 35 | 36 | it "should return length" do 37 | [{:vector => [0, 0, -1, 3], :expected => 3.162278}, 38 | {:vector => [11, 11, -7, 3], :expected => 17.320509}, 39 | {:vector => [-56, 23, 923, 9], :expected => 925.027039}].each do |data| 40 | vector = Geo3d::Vector.new *data[:vector] 41 | expected = data[:expected] 42 | Geo3d::Utils.float_cmp(vector.length, expected).should == true 43 | end 44 | end 45 | 46 | it "should return length squared" do 47 | [{:vector => [0, 0, -1, 3], :expected => 10}, 48 | {:vector => [11, 11, -7, 3], :expected => 300}, 49 | {:vector => [-56, 23, 923, 9], :expected => 855675}].each do |data| 50 | vector = Geo3d::Vector.new *data[:vector] 51 | expected = data[:expected] 52 | Geo3d::Utils.float_cmp(vector.length_squared, expected).should == true 53 | end 54 | end 55 | 56 | it "should be normalizable" do 57 | [{:vector => [0, 0, -1, 3], :expected => [0, 0, -0.316228, 0.948683]}, 58 | {:vector => [11, 11, -7, 3], :expected => [0.635085, 0.635085, -0.404145, 0.173205]}, 59 | {:vector => [-56, 23, 923, 9], :expected => [-0.060539, 0.024864, 0.997809, 0.009729]}].each do |data| 60 | vector = Geo3d::Vector.new(*data[:vector]).normalize 61 | expected = Geo3d::Vector.new *data[:expected] 62 | vector.should == expected 63 | end 64 | end 65 | 66 | it "should be able to linearly interpolate" do 67 | [{:a => [0, 0, 0, 0], :b => [1, 1, 1, 1], :interpolate_fraction => 0.5, :expected => [0.5, 0.5, 0.5, 0.5]}, 68 | {:a => [23, -3, 425, -332], :b => [-22, -45443, 886, 122], :interpolate_fraction => 0.21234433, :expected => [13.444505, -9651.926758, 522.890747, -235.595673]}].each do |data| 69 | a = Geo3d::Vector.new *data[:a] 70 | b = Geo3d::Vector.new *data[:b] 71 | expected = Geo3d::Vector.new *data[:expected] 72 | s = data[:interpolate_fraction] 73 | a.lerp(b, s).should == expected 74 | end 75 | end 76 | 77 | it "should calculate reflection" do 78 | [{:normal => [0, 1, 0], :incident => [1, 1, 0], :expected => [1, -1, 0]}, 79 | {:normal => [0, 1, 0], :incident => [0, -1, 0], :expected => [0, 1, 0]}, 80 | {:normal => [22, 33, 68], :incident => [65, -23, -23], :expected => [39357.000000, 58915.000000, 121425.000000, 0.000000]}].each do |data| 81 | normal = Geo3d::Vector.new *data[:normal] 82 | incident = Geo3d::Vector.new *data[:incident] 83 | expected = Geo3d::Vector.new *data[:expected] 84 | Geo3d::Vector.reflect(normal, incident).should == expected 85 | end 86 | end 87 | 88 | it "should calculate refraction" do 89 | [{:normal => [0, 1, 0], :incident => [1, 1, 0], :index_of_ref => 0.5, :expected => [0.5, -1, 0]}, 90 | {:normal => [0, 1, 0], :incident => [0, -1, 0], :index_of_ref => 21.345, :expected => [0, -1, 0]}, 91 | {:normal => [22, 33, 68], :incident => [65, -23, -23], :index_of_ref => 17.5678, :expected => [1142.121826, -403.737152, -403.395355]}, 92 | {:normal => [1, 0, 0], :incident => [0, 1, 0], :index_of_ref => 0.75, :expected => [-0.661438, 0.750000, 0.000000, 0.000000]} 93 | ].each do |data| 94 | 95 | normal = Geo3d::Vector.new *data[:normal] 96 | incident = Geo3d::Vector.new *data[:incident] 97 | expected = Geo3d::Vector.new *data[:expected] 98 | Geo3d::Vector.refract(normal, incident, data[:index_of_ref]).should == expected 99 | end 100 | end 101 | 102 | end -------------------------------------------------------------------------------- /lib/geo3d/quaternion.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | class Quaternion 3 | attr_reader :x, :y, :z, :w 4 | 5 | def initialize *args 6 | @x, @y, @z, @w = 0.0, 0.0, 0.0, 0.0 7 | @x = args[0].to_f if args.size > 0 8 | @y = args[1].to_f if args.size > 1 9 | @z = args[2].to_f if args.size > 2 10 | @w = args[3].to_f if args.size > 3 11 | end 12 | 13 | def to_a 14 | [x,y,z,w] 15 | end 16 | 17 | def x= v 18 | @x = v.to_f 19 | end 20 | 21 | def y= v 22 | @y = v.to_f 23 | end 24 | 25 | def z= v 26 | @z = v.to_f 27 | end 28 | 29 | def w= v 30 | @w = v.to_f 31 | end 32 | 33 | def +@ 34 | self.class.new x, y, z, w 35 | end 36 | 37 | def -@ 38 | self.class.new -x, -y, -z, -w 39 | end 40 | 41 | def == q 42 | Geo3d::Utils.float_cmp(x, q.x) && Geo3d::Utils.float_cmp(y, q.y) && Geo3d::Utils.float_cmp(z, q.z) && Geo3d::Utils.float_cmp(w, q.w) 43 | end 44 | 45 | def != vec 46 | !(self == vec) 47 | end 48 | 49 | def self.from_axis rotation_axis, radians = 0 50 | radians = Geo3d::Utils.normalize_angle radians #todo: is this cheating?.... 51 | normalized_rotation_axis = rotation_axis.zero_w.normalize 52 | q = self.new 53 | q.x = Math.sin(radians / 2.0) * normalized_rotation_axis.x 54 | q.y = Math.sin(radians / 2.0) * normalized_rotation_axis.y 55 | q.z = Math.sin(radians / 2.0) * normalized_rotation_axis.z 56 | q.w = Math.cos(radians / 2.0) 57 | q 58 | end 59 | 60 | def self.from_axis_degrees rotation_axis, degrees = 0 61 | from_axis rotation_axis, Geo3d::Utils.to_radians(degrees) 62 | end 63 | 64 | def self.from_matrix pm 65 | pout = self.new 66 | 67 | #puts "trace is #{pm.trace}" 68 | if false && pm.trace > 1.0 69 | sq_root_of_trace = Math.sqrt pm.trace 70 | pout.x = (pm._23 - pm._32) / (2.0 * sq_root_of_trace) 71 | pout.y = (pm._31 - pm._13) / (2.0 * sq_root_of_trace) 72 | pout.z = (pm._12- pm._21) / (2.0 * sq_root_of_trace) 73 | pout.w = sq_root_of_trace / 2.0 74 | #puts "a and pout is #{pout.inspect}" 75 | 76 | return pout 77 | end 78 | maxi = 0 79 | maxdiag = pm._11 80 | 81 | 82 | for i in 1..2 83 | if pm[i, i] > maxdiag #todo: indexing might need to be fixed > maxdiag 84 | maxi = i 85 | maxdiag = pm[i, i] #todo: indexing might need to be fixed 86 | end 87 | end 88 | case maxi 89 | when 0 90 | s = 2.0 * Math.sqrt(1.0 + pm._11 - pm._22 - pm._33) 91 | pout.x = 0.25 * s 92 | pout.y = (pm._12 + pm._21) / s 93 | pout.z = (pm._13 + pm._31) / s 94 | pout.w = (pm._23 - pm._32) / s 95 | 96 | when 1 97 | s = 2.0 * Math.sqrt(1.0 + pm._22 - pm._11 - pm._33) 98 | pout.x = (pm._12 + pm._21) / s 99 | pout.y = 0.25 * s 100 | pout.z = (pm._23 + pm._32) / s 101 | pout.w = (pm._31 - pm._13) / s 102 | 103 | when 2 104 | s = 2.0 * Math.sqrt(1.0 + pm._33 - pm._11 - pm._22) 105 | pout.x = (pm._13 + pm._31) / s 106 | pout.y = (pm._23 + pm._32) / s 107 | pout.z = 0.25 * s 108 | pout.w = (pm._12 - pm._21) / s 109 | end 110 | #puts "b" 111 | pout 112 | end 113 | 114 | def + quat 115 | self.class.new x + quat.x, y + quat.y, z + quat.z, w + quat.w 116 | end 117 | 118 | def - quat 119 | self.class.new x - quat.x, y - quat.y, z - quat.z, w - quat.w 120 | end 121 | 122 | def * v 123 | if Quaternion == v.class 124 | quat = v 125 | out = self.class.new 126 | out.w = w * quat.w - x * quat.x - y * quat.y - z * quat.z 127 | out.x = w * quat.x + x * quat.w + y * quat.z - z * quat.y 128 | out.y = w * quat.y - x * quat.z + y * quat.w + z * quat.x 129 | out.z = w * quat.z + x * quat.y - y * quat.x + z * quat.w 130 | out 131 | else 132 | self.class.new x*v, y*v, z*v, w*v 133 | end 134 | end 135 | 136 | def / v 137 | self.class.new x/v, y/v, z/v, w/v 138 | end 139 | 140 | def to_matrix 141 | v = normalize 142 | matrix = Matrix.identity 143 | matrix._11 = 1.0 - 2.0 * (v.y * v.y + v.z * v.z) 144 | matrix._12 = 2.0 * (v.x * v.y + v.z * v.w) 145 | matrix._13 = 2.0 * (v.x * v.z - v.y * v.w) 146 | matrix._21 = 2.0 * (v.x * v.y - v.z * v.w) 147 | matrix._22 = 1.0 - 2.0 * (v.x * v.x + v.z * v.z) 148 | matrix._23 = 2.0 * (v.y * v.z + v.x * v.w) 149 | matrix._31 = 2.0 * (v.x * v.z + v.y * v.w) 150 | matrix._32 = 2.0 * (v.y * v.z - v.x * v.w) 151 | matrix._33 = 1.0 - 2.0 * (v.x * v.x + v.y * v.y) 152 | matrix 153 | end 154 | 155 | def axis 156 | Vector.new( *(normalize / Math.sin( angle / 2.0 )).to_a ).zero_w 157 | end 158 | 159 | def angle 160 | Math.acos(normalize.w) * 2.0 161 | end 162 | 163 | def angle_degrees 164 | Geo3d::Utils.to_degrees angle 165 | end 166 | 167 | def length_squared 168 | dot self 169 | end 170 | 171 | def length 172 | Math.sqrt length_squared 173 | end 174 | 175 | def dot quat 176 | x * quat.x + y * quat.y + z * quat.z + w * quat.w 177 | end 178 | 179 | def normalize! 180 | len = length 181 | if length > 0 182 | @x /= len 183 | @y /= len 184 | @z /= len 185 | @w /= len 186 | end 187 | end 188 | 189 | def normalize 190 | q = self.class.new x, y, z, w 191 | q.normalize! 192 | q 193 | end 194 | 195 | def conjugate 196 | self.class.new -x, -y, -z, w 197 | end 198 | 199 | def inverse 200 | norm = length_squared 201 | if norm.zero? 202 | self.class.new 0, 0, 0, 0 203 | else 204 | conjugate / norm 205 | end 206 | end 207 | 208 | def identity? 209 | self == self.class.identity 210 | end 211 | 212 | def self.identity 213 | self.new 0, 0, 0, 1 214 | end 215 | end 216 | end -------------------------------------------------------------------------------- /spec/opengl/matrix_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require "opengl" 3 | require "glu" 4 | require "glut" 5 | 6 | # This spec tests the accuracy of Geo3D by comparing its calculations against the same calculations made in OpenGL 7 | 8 | describe Geo3d::Matrix do 9 | let(:gl){ Class.new{extend GL} } 10 | let(:glu){ Class.new{extend GLU} } 11 | 12 | before :all do 13 | GLUT.Init 14 | GLUT.InitDisplayMode(GLUT::DOUBLE | GLUT::RGB) 15 | GLUT.InitWindowSize(500, 500) 16 | GLUT.InitWindowPosition(100, 100) 17 | GLUT.CreateWindow('test') 18 | end 19 | 20 | before :each do 21 | gl.send :glMatrixMode, GL::PROJECTION 22 | gl.send :glLoadIdentity 23 | gl.send :glMatrixMode, GL::MODELVIEW 24 | gl.send :glLoadIdentity 25 | end 26 | 27 | 28 | it "the identity constructor should be functionally equivalent to glLoadIdentity" do 29 | gl_version = Geo3d::Matrix.new *gl.send(:glGetFloatv, GL::MODELVIEW_MATRIX).flatten 30 | Geo3d::Matrix.identity.should == gl_version 31 | end 32 | 33 | it "the right handed view constructor should be functionally equivalent to gluLookAt" do 34 | [{:eye => [1, 0, 0], :focus => [100, 0, -100], :up => [0, 1, 0]}].each do |data| 35 | eye = Geo3d::Vector.new *data[:eye] 36 | focus = Geo3d::Vector.new *data[:eye] 37 | up = Geo3d::Vector.new *data[:eye] 38 | 39 | glu.send :gluLookAt, eye.x, eye.y, eye.z, focus.x, focus.y, focus.z, up.x, up.y, up.z 40 | gl_version = Geo3d::Matrix.new *gl.send(:glGetFloatv, GL::MODELVIEW_MATRIX).flatten 41 | gl_version.should == Geo3d::Matrix.look_at_rh(eye, focus, up) 42 | end 43 | end 44 | 45 | it "gl_frustum should be equivalent to opengl glFrustum" do 46 | [{:l => -2, :r => 2, :b => -2, :t => 2, :zn => 1, :zf => 1000}, 47 | {:l => -231, :r => 453, :b => -232, :t => 2786, :zn => 9.221, :zf => 10000}].each do |data| 48 | 49 | gl.send :glMatrixMode, GL::PROJECTION 50 | gl.send :glLoadIdentity 51 | gl.send :glFrustum, data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 52 | 53 | gl_version = Geo3d::Matrix.new *(gl.send(:glGetFloatv, GL::PROJECTION_MATRIX).flatten) 54 | geo3d_matrix = Geo3d::Matrix.gl_frustum data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 55 | 56 | gl_version.should == geo3d_matrix 57 | end 58 | 59 | end 60 | 61 | it "glu_perspective_degrees should be equivalent to opengl gluPerspective" do 62 | [{:fovy_in_degrees => 60, :width => 640, :height => 480, :near => 0.1, :far => 1000}].each do |data| 63 | gl.send :glMatrixMode, GL::PROJECTION 64 | gl.send :glLoadIdentity 65 | glu.send :gluPerspective, data[:fovy_in_degrees], data[:width].to_f/data[:height].to_f, data[:near], data[:far] 66 | 67 | gl_version = Geo3d::Matrix.new *gl.send(:glGetFloatv, GL::PROJECTION_MATRIX).flatten 68 | geo3d_matrix = Geo3d::Matrix.glu_perspective_degrees(data[:fovy_in_degrees], data[:width].to_f/data[:height].to_f, data[:near], data[:far]) 69 | 70 | gl_version.should == geo3d_matrix 71 | end 72 | end 73 | 74 | it "gl_ortho should be equivalent to opengl glOrtho" do 75 | [{:l => -2, :r => 2, :b => -2, :t => 2, :zn => 1, :zf => 1000}, 76 | {:l => -231, :r => 453, :b => -232, :t => 2786, :zn => 9.221, :zf => 10000}].each do |data| 77 | gl.send :glMatrixMode, GL::PROJECTION 78 | gl.send :glLoadIdentity 79 | gl.send :glOrtho, data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 80 | 81 | gl_version = Geo3d::Matrix.new *(gl.send(:glGetFloatv, GL::PROJECTION_MATRIX).flatten) 82 | geo3d_matrix = Geo3d::Matrix.gl_ortho data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 83 | 84 | gl_version.should == geo3d_matrix 85 | end 86 | end 87 | 88 | it "should multiply matrices the same way opengl does" do 89 | 10000.times do 90 | a_values = (0..15).to_a.map do |i| 91 | rand * rand(100) 92 | end 93 | 94 | b_values = (0..15).to_a.map do |i| 95 | rand * rand(100) 96 | end 97 | 98 | geo3d_matrix = Geo3d::Matrix.new(*b_values) * Geo3d::Matrix.new(*a_values) 99 | 100 | gl.send :glMatrixMode, GL::MODELVIEW 101 | gl.send :glLoadMatrixf, a_values 102 | gl.send :glMultMatrixf, b_values 103 | gl_version = Geo3d::Matrix.new *gl.send(:glGetFloatv, GL::MODELVIEW).flatten 104 | 105 | gl_version.should == geo3d_matrix 106 | end 107 | end 108 | 109 | it "should project a vector the same way gluProject does" do 110 | viewport_data = {:x => 0, :y => 0, :width => 640, :height => 480} 111 | projection_data = {:fovy_in_degrees => 60.0, :width => 640.0, :height => 480.0, :near => 0.1, :far => 1000.0} 112 | view_data = {:eye => [1.0, 0.0, 0.0], :focus => [200.0, -40.0, -100.0], :up => [0.0, 1.0, 0.0]} 113 | eye = Geo3d::Vector.new *view_data[:eye] 114 | focus = Geo3d::Vector.new *view_data[:focus] 115 | up = Geo3d::Vector.new *view_data[:up] 116 | 117 | 118 | viewport_matrix = Geo3d::Matrix.viewport viewport_data[:x], viewport_data[:y], viewport_data[:width], viewport_data[:height] 119 | projection_matrix = Geo3d::Matrix.glu_perspective_degrees(projection_data[:fovy_in_degrees], projection_data[:width].to_f/projection_data[:height].to_f, projection_data[:near], projection_data[:far]) 120 | view_matrix = Geo3d::Matrix.look_at_rh(eye, focus, up) 121 | 122 | vector = Geo3d::Vector.new 300, 100, -500 123 | 124 | glu_vector = glu.send :gluProject, vector.x, vector.y, vector.z, projection_matrix.to_a, view_matrix.to_a, [viewport_data[:x], viewport_data[:y], viewport_data[:width], viewport_data[:height]] 125 | 126 | Geo3d::Vector.new(*glu_vector).should == vector.project(viewport_matrix, projection_matrix, view_matrix, Geo3d::Matrix.identity).zero_w 127 | end 128 | 129 | it "should unproject a vector the same way gluUnproject does" do 130 | viewport_data = {:x => 0, :y => 0, :width => 640, :height => 480} 131 | projection_data = {:fovy_in_degrees => 60.0, :width => 640.0, :height => 480.0, :near => 0.1, :far => 1000.0} 132 | view_data = {:eye => [1.0, 0.0, 0.0], :focus => [200.0, -40.0, -100.0], :up => [0.0, 1.0, 0.0]} 133 | eye = Geo3d::Vector.new *view_data[:eye] 134 | focus = Geo3d::Vector.new *view_data[:focus] 135 | up = Geo3d::Vector.new *view_data[:up] 136 | 137 | 138 | viewport_matrix = Geo3d::Matrix.viewport viewport_data[:x], viewport_data[:y], viewport_data[:width], viewport_data[:height] 139 | projection_matrix = Geo3d::Matrix.glu_perspective_degrees(projection_data[:fovy_in_degrees], projection_data[:width].to_f/projection_data[:height].to_f, projection_data[:near], projection_data[:far]) 140 | view_matrix = Geo3d::Matrix.look_at_rh(eye, focus, up) 141 | 142 | vector = Geo3d::Vector.new 574.1784279190967, 294.42147391181595, 0.8485367205965038 143 | 144 | glu_vector = glu.send :gluUnProject, vector.x, vector.y, vector.z, projection_matrix.to_a, view_matrix.to_a, [viewport_data[:x], viewport_data[:y], viewport_data[:width], viewport_data[:height]] 145 | Geo3d::Vector.new(*glu_vector).should == vector.unproject(viewport_matrix, projection_matrix, view_matrix, Geo3d::Matrix.identity).zero_w 146 | end 147 | 148 | 149 | end -------------------------------------------------------------------------------- /spec/lib/quaternion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Geo3d::Quaternion do 4 | it "should default all values to zero" do 5 | q = Geo3d::Quaternion.new 6 | q.x.zero?.should == true 7 | q.y.zero?.should == true 8 | q.z.zero?.should == true 9 | q.w.zero?.should == true 10 | end 11 | 12 | it "should be constructable from an axis and angle" do 13 | [{:axis => [0, 1, 0], :angle => 1, :expected => [0.000000, 0.479426, 0.000000, 0.877583]}, 14 | {:axis => [0, -1, 0], :angle => 1, :expected => [0.000000, -0.479426, 0.000000, 0.877583]}, 15 | {:axis => [0, 1, 0], :angle => -1, :expected => [0.000000, -0.479426, 0.000000, 0.877583]}, 16 | {:axis => [0, 1, 0], :angle => -6, :expected => [-0.000000, -0.141120, -0.000000, -0.989992]}, 17 | {:axis => [-213, 133, 22, -232], :angle => -3432, :expected => [0.538065, -0.335975, -0.055575, 0.771050]}].each do |data| 18 | Geo3d::Quaternion.from_axis(Geo3d::Vector.new(*data[:axis]), data[:angle]).should == Geo3d::Quaternion.new(*data[:expected]) 19 | end 20 | end 21 | 22 | it "should be constructable from a rotation matrix" do 23 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_x 1).should == Geo3d::Quaternion.new(0.479426, 0.000000, -0.000000, 0.877583) 24 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_y 1).should == Geo3d::Quaternion.new(-0.000000, 0.479426, -0.000000, 0.877583) 25 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_z 1).should == Geo3d::Quaternion.new(-0.000000, 0.000000, 0.479426, 0.877583) 26 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_x 3.2).should == Geo3d::Quaternion.new(0.999574, 0.000000, 0.000000, -0.029200) 27 | end 28 | 29 | it "should be able to construct as the identity quaternion" do 30 | q = Geo3d::Quaternion.identity 31 | q.should == Geo3d::Quaternion.new(0, 0, 0, 1) 32 | q.identity?.should == true 33 | end 34 | 35 | it "should be able to convert to a rotation matrix" do 36 | [{:quaternion => [0.0, 0.7071067811865475, 0.0, 0.7071067811865475], :expected => [0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}].each do |data| 37 | quaternion = Geo3d::Quaternion.new *data[:quaternion] 38 | data[:expected].size.should == 16 39 | expected = Geo3d::Matrix.new *data[:expected] 40 | quaternion.to_matrix.should == expected 41 | end 42 | end 43 | 44 | it "should return axis and angle of rotation" do 45 | for i in 0..10000 46 | angle = 0.1 * i + 0.1 47 | normalized_angle = Geo3d::Utils.normalize_angle angle 48 | 49 | Geo3d::Quaternion.from_axis(Geo3d::Vector.new(1, 0, 0), angle).axis.should == Geo3d::Vector.new(1, 0, 0) 50 | Geo3d::Quaternion.from_axis(Geo3d::Vector.new(0, 1, 0), angle).axis.should == Geo3d::Vector.new(0, 1, 0) 51 | Geo3d::Quaternion.from_axis(Geo3d::Vector.new(0, 0, 1), angle).axis.should == Geo3d::Vector.new(0, 0, 1) 52 | 53 | 54 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_x angle).axis.should == Geo3d::Vector.new(1, 0, 0) 55 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_y angle).axis.should == Geo3d::Vector.new(0, 1, 0) 56 | Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_z angle).axis.should == Geo3d::Vector.new(0, 0, 1) 57 | 58 | Geo3d::Utils.float_cmp(Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_x angle).angle, normalized_angle).should == true 59 | Geo3d::Utils.float_cmp(Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_y angle).angle, normalized_angle).should == true 60 | Geo3d::Utils.float_cmp(Geo3d::Quaternion.from_matrix(Geo3d::Matrix.rotation_z angle).angle, normalized_angle).should == true 61 | end 62 | end 63 | 64 | it "should return conjugate" do 65 | [{:quaternion => [1, 1, 1, 1], :expected => [-1, -1, -1, 1]}].each do |data| 66 | quaternion = Geo3d::Quaternion.new *data[:quaternion] 67 | expected = Geo3d::Quaternion.new *data[:expected] 68 | quaternion.conjugate.should == expected 69 | end 70 | end 71 | 72 | it "should return inverse" do 73 | [{:quaternion => [1, 1, 1, 1], :expected => [-0.250000, -0.250000, -0.250000, 0.250000]}, 74 | {:quaternion => [-1, -1, -1, -1], :expected => [0.250000, 0.250000, 0.250000, -0.250000]}].each do |data| 75 | quaternion = Geo3d::Quaternion.new *data[:quaternion] 76 | expected = Geo3d::Quaternion.new *data[:expected] 77 | quaternion.inverse.should == expected 78 | end 79 | end 80 | 81 | it "should support dot products with other quaternions" do 82 | [{:a => [1, 1, 1, 1], :b => [2, 2, 2, 2], :expected => 8}, 83 | {:a => [0, 1, 0, -99], :b => [0, -42, 2, 52], :expected => -5190}, 84 | {:a => [91, -2731, 1, 123], :b => [2, 7, -9, 2], :expected => -18698}, 85 | ].each do |data| 86 | a = Geo3d::Quaternion.new *data[:a] 87 | b = Geo3d::Quaternion.new *data[:b] 88 | Geo3d::Utils.float_cmp(a.dot(b), data[:expected]).should == true 89 | end 90 | end 91 | 92 | it "should return length" do 93 | [{:quaternion => [0, 0, -1, 3], :expected => 3.162278}, 94 | {:quaternion => [11, 11, -7, 3], :expected => 17.320509}, 95 | {:quaternion => [-56, 23, 923, 9], :expected => 925.027039}].each do |data| 96 | quaternion = Geo3d::Quaternion.new *data[:quaternion] 97 | expected = data[:expected] 98 | Geo3d::Utils.float_cmp(quaternion.length, expected).should == true 99 | end 100 | end 101 | 102 | it "should return length squared" do 103 | [{:quaternion => [0, 0, -1, 3], :expected => 10}, 104 | {:quaternion => [11, 11, -7, 3], :expected => 300}, 105 | {:quaternion => [-56, 23, 923, 9], :expected => 855675}].each do |data| 106 | quaternion = Geo3d::Quaternion.new *data[:quaternion] 107 | expected = data[:expected] 108 | Geo3d::Utils.float_cmp(quaternion.length_squared, expected).should == true 109 | end 110 | end 111 | 112 | it "should be normalizable" do 113 | [{:quaternion => [0, 0, -1, 3], :expected => [0, 0, -0.316228, 0.948683]}, 114 | {:quaternion => [11, 11, -7, 3], :expected => [0.635085, 0.635085, -0.404145, 0.173205]}, 115 | {:quaternion => [-56, 23, 923, 9], :expected => [-0.060539, 0.024864, 0.997809, 0.009729]}].each do |data| 116 | quaternion = Geo3d::Quaternion.new(*data[:quaternion]).normalize 117 | expected = Geo3d::Quaternion.new *data[:expected] 118 | quaternion.should == expected 119 | end 120 | end 121 | 122 | 123 | it "multiplying two quaternions with same axis should result in a quaternion with the same axis and the sum of their angles" do 124 | [[1, 0, 0], [0, 1, 0], [0, 0, 1], [2, 2, 8]].each do |axis| 125 | axis = Geo3d::Vector.new *axis 126 | 127 | [[2.3, 0.4], 128 | [2.3, 0.4, -0.1], 129 | [-1, -0.25, -0.5, 2.3]].each do |angles| 130 | product = nil 131 | angles.each do |angle| 132 | q = Geo3d::Quaternion.from_axis axis, angle 133 | if product.nil? 134 | product = q 135 | else 136 | product *= q 137 | end 138 | end 139 | 140 | Geo3d::Utils.float_cmp(product.angle, angles.inject(:+)).should == true 141 | product.axis.should == axis.normalize 142 | end 143 | end 144 | end 145 | 146 | 147 | 148 | 149 | #todo: add tests for quaternion interpolation 150 | 151 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geo3d 2 | 3 | Library for common 3d graphics vector and matrix operations 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'geo3d' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install geo3d 18 | 19 | ## Usage 20 | ``` 21 | a = Geo3d::Vector.point 1, 0, 0 22 | b = Geo3d::Vector.point 0, 1, 0 23 | sum = a + b # add them together 24 | sum *= 2 #double the vector 25 | 26 | m = Geo3d::Matrix.translation 0, 5, 0, #create a translation matrix that transforms a points 5 units on the y-axis 27 | sum = m * sum #apply the transform to our vector 28 | ``` 29 | 30 | 31 | ## Vector 32 | 33 | Describes a three dimensional point or direction. A vector has the following read/write attributes: x, y, z, w 34 | 35 | Constructors 36 | ``` 37 | a = Geo3d::Vector.new #all attributes are initialized to zero 38 | b = Geo3d::Vector.new x,y,z,w #initialize all attributes directly 39 | c = Geo3d::Vector.new x,y,z #initialize x,y, and z directly and default w to zero 40 | d = Geo3d::Vector.point x,y,z #initialize x,y, and z directly and default w to one 41 | e = Geo3d::Vector.direction x,y,z #initialize x,y, and z directly and default w to zero 42 | ``` 43 | 44 | Vectors are overloaded with all of the basic math operations. 45 | 46 | Addition 47 | ``` 48 | vec_a + vec_b 49 | ``` 50 | Subtraction 51 | ``` 52 | vec_a - vec_b 53 | ``` 54 | Multiplication 55 | ``` 56 | vec * scalar 57 | ``` 58 | Division 59 | ``` 60 | vec / scalar 61 | ``` 62 | 63 | Additional vector operations 64 | 65 | Dot product 66 | ``` 67 | vec.dot 68 | ``` 69 | Cross product 70 | ``` 71 | vec_a.cross vec_b 72 | ``` 73 | Magnitude 74 | ``` 75 | vec.length 76 | ``` 77 | Squared Magnitude 78 | ``` 79 | vec.length_squared 80 | ``` 81 | Normalize 82 | ``` 83 | vec.normalize #returns a normalized version of the vector 84 | vec.normalize! #normalizes the vector in place 85 | ``` 86 | Linear Interpolation 87 | ``` 88 | vec_a.lerp vec_b, 0.4 #returns a new vector which is the 40% linear interpolation between vec_a and vec_b 89 | ``` 90 | Screenspace projections 91 | ``` 92 | vec.project viewport, projection, view, world #transform an objectspace vertex to screenspace 93 | vec.unproject viewport, projection, view, world #transform a screenspace vertex to objectspace 94 | ``` 95 | Reflections 96 | ``` 97 | vec.reflect normal, incident 98 | ``` 99 | Refractions 100 | ``` 101 | vec.refract normal, incident, index_of_refraction 102 | ``` 103 | 104 | ## Matrix 105 | 106 | A 4x4 matrix used for transforming vectors. Elements can be read/written to with the double subscription operation. 107 | For instance, matrix[0,1] = 7 writes seven to the element in column zero and row one. 108 | 109 | Matrices are overloaded with all of the basic math operations 110 | 111 | Addition 112 | ``` 113 | mat_a + mat_b 114 | ``` 115 | Subtraction 116 | ``` 117 | mat_a - mat_b 118 | ``` 119 | Scalar Multiplication 120 | ``` 121 | mat * scalar 122 | ``` 123 | Scalar Division 124 | ``` 125 | mat / scalar 126 | ``` 127 | Matrix Multiplication 128 | ``` 129 | mat_a * mat_b 130 | ``` 131 | Matrix Vector Multiplication 132 | ``` 133 | mat * vec 134 | ``` 135 | 136 | 137 | Additional matrix operations 138 | 139 | Inverse 140 | ``` 141 | mat.inverse #returns inverse of matrix 142 | mat.inverse true #returns inverse of matrix along with its determinant 143 | mat.determinant #returns the determinant 144 | ``` 145 | Transpose 146 | ``` 147 | mat.transpose 148 | ``` 149 | 150 | Common matrix constructors 151 | 152 | Identity 153 | ``` 154 | Geo3d::Matrix.identity #returns the identity matrix 155 | ``` 156 | Translation 157 | ``` 158 | Geo3d::Matrix.translation x,y,z #returns a translation matrix 159 | ``` 160 | Scaling 161 | ``` 162 | Geo3d::Matrix.scaling x,y,z #returns a scaling matrix 163 | Geo3d::Matrix.uniform_scaling scale #returns a uniform scaling matrix 164 | ``` 165 | Rotation 166 | ``` 167 | Geo3d::Matrix.rotation_x 0.44 #rotate .44 radians about x axis 168 | Geo3d::Matrix.rotation_y 0.44 #rotate .44 radians about y axis 169 | Geo3d::Matrix.rotation_z 0.44 #rotate .44 radians about z axis 170 | 171 | axis = Geo3d::Vector.new 1,1,0 172 | angle = 0.9 173 | Geo3d::Matrix.rotation axis, angle #rotate about an arbitrary axis 174 | ``` 175 | Projection matrix constructors ala Direct3D (clip space of z coordinate has a range of 0 to 1) 176 | ``` 177 | Geo3d::Matrix.perspective_fov_rh fovy, aspect, z_near, z_far #returns a right handed perspective projection matrix 178 | Geo3d::Matrix.perspective_fov_lh fovy, aspect, z_near, z_far #returns a left handed perspective projection matrix 179 | Geo3d::Matrix.ortho_off_center_rh left, right, bottom, top, z_near, z_far #returns a right handed orthographic projection matrix 180 | Geo3d::Matrix.ortho_off_center_lh left, right, bottom, top, z_near, z_far #returns a left handed orthographic projection matrix 181 | ``` 182 | Projection matrix constructors ala OpenGL (clip space of z coordinate has a range of -1 to 1) 183 | ``` 184 | Geo3d::Matrix.glu_perspective_degrees fovy, aspect, zn, zf #returns an opengl style right handed perspective projection matrix 185 | Geo3d::Matrix.gl_frustum l, r, b, t, zn, zf #returns an opengl style right handed perspective projection matrix 186 | Geo3d::Matrix.gl_ortho l, r, b, t, zn, zf #returns an opengl style righthanded orthographic projection matrix 187 | ``` 188 | View matrix constructors 189 | ``` 190 | Geo3d::Matrix.look_at_rh eye_position, look_at_position, up_direction #returns a right handed view matrix 191 | Geo3d::Matrix.look_at_lh eye_position, look_at_position, up_direction #returns a left handed view matrix 192 | ``` 193 | Viewport matrix constructors 194 | ``` 195 | Geo3d::Matrix.viewport x, y, width, height 196 | ``` 197 | Misc constructors 198 | ``` 199 | Geo3d::Matrix.reflection reflection_plane #returns a reflection matrix where reflection_plane is a Geo3d::Vector that corresponds to the normal of the plane 200 | Geo3d::Matrix.shadow light_position, plane #returns a shadow matrix 201 | ``` 202 | Matrix Decomposition 203 | ``` 204 | matrix.scaling_component 205 | matrix.translation_component 206 | matrix.rotation_component 207 | ``` 208 | 209 | ## Plane 210 | 211 | Represents a 2d surface in three dimensional space. Has the attributes a,b,c,d that mirror the standard plane equations. 212 | 213 | There are a couple constructors to build planes from points and normals. 214 | ```` 215 | Geo3d::Plane.from_points pv1, pv2, pv3 #builds a plane from known points on the plane 216 | Geo3d::Plane.from_point_and_normal point, normal #builds a plane from it's normal and a known point 217 | ```` 218 | 219 | Additional plane operations 220 | 221 | Dot product 222 | ``` 223 | plane.dot v #v can be a vector or another plane 224 | ``` 225 | Normalize 226 | ``` 227 | plane.normalize #returns a normalized version of the plane 228 | plane.normalize! #normalizes the plane in place 229 | ``` 230 | Normal 231 | ``` 232 | plane.normal #returns the normal of the plane 233 | ``` 234 | Line intersection 235 | ``` 236 | plane.line_intersection line_start, line_end #returns the intersection of the line onto the plane 237 | ```` 238 | Plane Transformation 239 | ```` 240 | #transforms plane by the matrix, if use_inverse_transpose is set to true, the plane will be transformed by the inverse transpose of matrix 241 | plane.transform matrix, use_inverse_transpose = true 242 | ```` 243 | 244 | 245 | ## Quaternion 246 | 247 | A mathematical construct to represent rotations in 3d space. 248 | 249 | Quaternions support all the basic math operations. 250 | 251 | Addition 252 | ``` 253 | quat_a + quat_b 254 | ``` 255 | Subtraction 256 | ``` 257 | quat_a - quat_b 258 | ``` 259 | Quaternion Multiplication 260 | ``` 261 | quat_a * quat_b 262 | ``` 263 | Scalar Multiplication 264 | ``` 265 | quat * scalar 266 | ``` 267 | Scalar Division 268 | ``` 269 | quat / scalar 270 | ``` 271 | Getting axis and angle 272 | ``` 273 | quat.axis 274 | quat.angle #returns angle in radians 275 | quat.angle_degrees #returns angle in degrees 276 | ``` 277 | Converting to a matrix 278 | ``` 279 | quat.to_matrix 280 | ``` 281 | 282 | Additional quaternion operations 283 | Magnitude 284 | ``` 285 | quat.length 286 | ``` 287 | Squared Magnitude 288 | ``` 289 | quat.length_squared 290 | ``` 291 | Normalize 292 | ``` 293 | quat.normalize #returns a normalized version of the quaternion 294 | quat.normalize! #normalizes the quaternion in place 295 | ``` 296 | Inverse 297 | ``` 298 | quat.inverse #returns inverse of quaternion 299 | ``` 300 | Conjugate 301 | ``` 302 | quat.conjugate 303 | ``` 304 | Dot product 305 | ``` 306 | quat.dot 307 | ``` 308 | 309 | Constructors 310 | ``` 311 | Geo3d::Quaternion.from_axis rotation_axis, radians #returns a quaternion from an axis and angle 312 | Geo3d::Quaternion.from_matrix m #returns a quaternion from a rotation matrix 313 | Geo3d::Quaternion.identity #returns the identity quaternion 314 | 315 | ``` 316 | 317 | 318 | ## Triangle 319 | 320 | Represents a triangle in three dimensional space 321 | 322 | Constructors 323 | ``` 324 | Geo3d::Triangle.from_axis rotation_axis, radians #returns a quaternion from an axis and angle 325 | Geo3d::Quaternion.from_matrix m #returns a quaternion from a rotation matrix 326 | Geo3d::Quaternion.identity #returns the identity quaternion 327 | ``` 328 | Normal 329 | ``` 330 | triangle.normal #returns the normal of the plane 331 | ``` 332 | Winding 333 | ``` 334 | triangle.clockwise? #is the triangle winded clockwise? 335 | triangle.counter_clockwise? #is the triangle winded counter clockwise? 336 | ``` 337 | Flipping 338 | ``` 339 | triangle.flip #returns a flipped version of the triangle (reverses the winding) 340 | triangle.flip! #flips the triangle in place 341 | ``` 342 | signed area 343 | ``` 344 | triangle.signed_area 345 | ``` 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | ## Contributing 357 | 358 | 1. Fork it 359 | 2. Create your feature branch (`git checkout -b my-new-feature`) 360 | 3. Commit your changes (`git commit -am 'Add some feature'`) 361 | 4. Push to the branch (`git push origin my-new-feature`) 362 | 5. Create new Pull Request 363 | -------------------------------------------------------------------------------- /spec/lib/matrix_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Geo3d::Matrix do 4 | def random_matrix 5 | r = Geo3d::Matrix.new 6 | for i in 0..3 7 | for j in 0..3 8 | r[i, j] = rand 10000 9 | end 10 | end 11 | r 12 | end 13 | 14 | def random_vector 15 | v = Geo3d::Vector.new 16 | v.x = rand 10000 17 | v.y = rand 10000 18 | v.z = rand 10000 19 | v.w = rand 10000 20 | v 21 | end 22 | 23 | 24 | it "should default all values to zero" do 25 | Geo3d::Matrix.new.to_a.select(&:zero?).size.should == 16 26 | end 27 | 28 | it "should be able to extract translation component" do 29 | translation = Geo3d::Vector.new 3,-4,6 30 | matrix = Geo3d::Matrix.translation translation.x, translation.y, translation.z 31 | matrix.translation_component.should == translation 32 | end 33 | 34 | it "should be able to extract scaling component" do 35 | scaling = Geo3d::Vector.new 3,4,6 36 | matrix = Geo3d::Matrix.scaling scaling.x, scaling.y, scaling.z 37 | matrix.scaling_component.should == scaling 38 | end 39 | 40 | it "should be able to extract rotation component" do 41 | angle = 2.234 42 | matrix = Geo3d::Matrix.rotation_z angle 43 | matrix.rotation_component.should == Geo3d::Quaternion.from_axis(Geo3d::Vector.new(0,0,1), angle) 44 | end 45 | 46 | it "should be invertible" do 47 | 100.times do 48 | r = random_matrix 49 | (r * r.inverse).identity?.should == true 50 | end 51 | end 52 | 53 | it "should return the determinant" do 54 | [{:matrix => [0.321046, 0.000000, 0.000000, 0.000000, 0.000000, 0.642093, 0.000000, 0.000000, 0.000000, 0.000000, -1.000095, -1.000000, 0.000000, 0.000000, -2.000190, 0.000000], :expected => -0.412322}, 55 | {:matrix => [1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, -2.000000, 0.000000, 1.000000], :expected => -1}, 56 | {:matrix => [-0.392804, -0.878379, -0.272314, 0.000000, 0.000000, 0.296115, -0.955152, 0.000000, 0.919622, -0.375187, -0.116315, 0.000000, -2.366064, 1.411711, 2.531564, 1.000000], :expected => 1}].each do |data| 57 | data[:matrix].size.should == 16 58 | Geo3d::Utils.float_cmp( Geo3d::Matrix.new(*data[:matrix]).determinant, data[:expected] ).should == true 59 | end 60 | end 61 | 62 | it "should be transposable" do 63 | 64 | end 65 | 66 | it "should have an identity constructor" do 67 | identity = Geo3d::Matrix.identity 68 | identity.identity?.should == true 69 | end 70 | 71 | it "should have a right handed perspective projection constructor" do 72 | [{:fovy => 2, :aspect => 2, :zn => 2, :zf => 21000, :expected => [0.321046, 0.000000, 0.000000, 0.000000, 0.000000, 0.642093, 0.000000, 0.000000, 0.000000, 0.000000, -1.000095, -1.000000, 0.000000, 0.000000, -2.000190, 0.000000]}].each do |data| 73 | matrix = Geo3d::Matrix.perspective_fov_rh data[:fovy], data[:aspect], data[:zn], data[:zf] 74 | data[:expected].size.should == 16 75 | expected = Geo3d::Matrix.new *data[:expected] 76 | matrix.should == expected 77 | end 78 | end 79 | 80 | 81 | it "should have a left handed perspective projection constructor" do 82 | [{:fovy => 2, :aspect => 2, :zn => 2, :zf => 21000, :expected => [0.321046, 0.000000, 0.000000, 0.000000, 0.000000, 0.642093, 0.000000, 0.000000, 0.000000, 0.000000, 1.000095, 1.000000, 0.000000, 0.000000, -2.000190, 0.000000]}].each do |data| 83 | matrix = Geo3d::Matrix.perspective_fov_lh data[:fovy], data[:aspect], data[:zn], data[:zf] 84 | data[:expected].size.should == 16 85 | expected = Geo3d::Matrix.new *data[:expected] 86 | matrix.should == expected 87 | end 88 | end 89 | 90 | 91 | it "should have a right handed orthographic projection constructor" do 92 | [{:l => -100, :r => 100, :b => -200, :t => 200, :zn => 1, :zf => 2000, :expected => [0.010000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005000, 0.000000, 0.000000, 0.000000, 0.000000, -0.000500, 0.000000, -0.000000, -0.000000, -0.000500, 1.000000]}].each do |data| 93 | matrix = Geo3d::Matrix.ortho_off_center_rh data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 94 | data[:expected].size.should == 16 95 | expected = Geo3d::Matrix.new *data[:expected] 96 | matrix.should == expected 97 | end 98 | end 99 | 100 | 101 | it "should have a left handed orthographic projection constructor" do 102 | [{:l => -100, :r => 100, :b => -200, :t => 200, :zn => 1, :zf => 2000, :expected => [0.010000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000500, 0.000000, -0.000000, -0.000000, -0.000500, 1.000000]}].each do |data| 103 | matrix = Geo3d::Matrix.ortho_off_center_lh data[:l], data[:r], data[:b], data[:t], data[:zn], data[:zf] 104 | data[:expected].size.should == 16 105 | expected = Geo3d::Matrix.new *data[:expected] 106 | matrix.should == expected 107 | end 108 | end 109 | 110 | 111 | it "should have a right handed view constructor" do 112 | [{:eye => [1, 2, 3], :focus => [200, 700, 88], :up => [0, 1, 0], :expected => [-0.392804, -0.878379, -0.272314, 0.000000, 0.000000, 0.296115, -0.955152, 0.000000, 0.919622, -0.375187, -0.116315, 0.000000, -2.366064, 1.411711, 2.531564, 1.000000]}].each do |data| 113 | matrix = Geo3d::Matrix.look_at_rh Geo3d::Vector.new(*data[:eye]), Geo3d::Vector.new(*data[:focus]), Geo3d::Vector.new(*data[:up]) 114 | data[:expected].size.should == 16 115 | expected = Geo3d::Matrix.new *data[:expected] 116 | matrix.should == expected 117 | end 118 | end 119 | 120 | 121 | it "should have a left handed view constructor" do 122 | [{:eye => [1, 2, 3], :focus => [200, 700, 88], :up => [0, 1, 0], :expected => [0.392804, -0.878379, 0.272314, 0.000000, 0.000000, 0.296115, 0.955152, 0.000000, -0.919622, -0.375187, 0.116315, 0.000000, 2.366064, 1.411711, -2.531564, 1.000000]}].each do |data| 123 | matrix = Geo3d::Matrix.look_at_lh Geo3d::Vector.new(*data[:eye]), Geo3d::Vector.new(*data[:focus]), Geo3d::Vector.new(*data[:up]) 124 | data[:expected].size.should == 16 125 | expected = Geo3d::Matrix.new *data[:expected] 126 | matrix.should == expected 127 | end 128 | end 129 | 130 | it "should have a translation constructor" do 131 | 10.times do 132 | random_translation = random_vector 133 | matrix = Geo3d::Matrix.translation random_translation.x, random_translation.y, random_translation.z 134 | 10.times do 135 | random_vec = random_vector.one_w 136 | Geo3d::Utils.float_cmp((matrix * random_vec).x, random_vec.x + random_translation.x).should == true 137 | Geo3d::Utils.float_cmp((matrix * random_vec).y, random_vec.y + random_translation.y).should == true 138 | Geo3d::Utils.float_cmp((matrix * random_vec).z, random_vec.z + random_translation.z).should == true 139 | Geo3d::Utils.float_cmp((matrix * random_vec).w, 1).should == true 140 | end 141 | end 142 | end 143 | 144 | it "should have a scaling constructor" do 145 | 10.times do 146 | random_scaling = random_vector 147 | matrix = Geo3d::Matrix.scaling random_scaling.x, random_scaling.y, random_scaling.z 148 | 10.times do 149 | random_vec = random_vector.one_w 150 | Geo3d::Utils.float_cmp((matrix * random_vec).x, random_vec.x * random_scaling.x).should == true 151 | Geo3d::Utils.float_cmp((matrix * random_vec).y, random_vec.y * random_scaling.y).should == true 152 | Geo3d::Utils.float_cmp((matrix * random_vec).z, random_vec.z * random_scaling.z).should == true 153 | Geo3d::Utils.float_cmp((matrix * random_vec).w, 1).should == true 154 | end 155 | end 156 | end 157 | 158 | 159 | it "should have an x-axis rotation constructor" do 160 | [{:angle => 1, :expected => [1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.540302, 0.841471, 0.000000, 0.000000, -0.841471, 0.540302, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}, 161 | {:angle => 3.2, :expected => [1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -0.998295, -0.058374, 0.000000, 0.000000, 0.058374, -0.998295, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}].each do |data| 162 | matrix = Geo3d::Matrix.rotation_x data[:angle] 163 | data[:expected].size.should == 16 164 | expected = Geo3d::Matrix.new *data[:expected] 165 | matrix.should == expected 166 | matrix.is_rotation_transform?.should == true 167 | expected.is_rotation_transform?.should == true 168 | end 169 | end 170 | 171 | it "should have an y-axis rotation constructor" do 172 | [{:angle => 1, :expected => [0.540302, 0.000000, -0.841471, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.841471, 0.000000, 0.540302, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}, 173 | {:angle => 3.2, :expected => [-0.998295, 0.000000, 0.058374, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, -0.058374, 0.000000, -0.998295, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}].each do |data| 174 | matrix = Geo3d::Matrix.rotation_y data[:angle] 175 | data[:expected].size.should == 16 176 | expected = Geo3d::Matrix.new *data[:expected] 177 | matrix.should == expected 178 | matrix.is_rotation_transform?.should == true 179 | expected.is_rotation_transform?.should == true 180 | end 181 | end 182 | 183 | it "should have a z-axis rotation constructor" do 184 | [{:angle => 1, :expected => [0.540302, 0.841471, 0.000000, 0.000000, -0.841471, 0.540302, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}, 185 | {:angle => 3.2, :expected => [-0.998295, -0.058374, 0.000000, 0.000000, 0.058374, -0.998295, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}].each do |data| 186 | matrix = Geo3d::Matrix.rotation_z data[:angle] 187 | data[:expected].size.should == 16 188 | expected = Geo3d::Matrix.new *data[:expected] 189 | matrix.should == expected 190 | matrix.is_rotation_transform?.should == true 191 | expected.is_rotation_transform?.should == true 192 | end 193 | end 194 | 195 | it "should have an arbitrary axis rotation constructor" do 196 | [{:axis => [1, 1, 0], :angle => 88.7, :expected => [0.870782, 0.129218, -0.474385, 0.000000, 0.129218, 0.870782, 0.474385, 0.000000, 0.474385, -0.474385, 0.741564, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}].each do |data| 197 | matrix = Geo3d::Matrix.rotation Geo3d::Vector.new(*data[:axis]), data[:angle] 198 | data[:expected].size.should == 16 199 | expected = Geo3d::Matrix.new *data[:expected] 200 | matrix.should == expected 201 | matrix.is_rotation_transform?.should == true 202 | expected.is_rotation_transform?.should == true 203 | end 204 | end 205 | 206 | it "should have a reflection constructor" do 207 | [{:plane => [0, 1, 0, 0], :expected => [1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000]}, 208 | {:plane => [0, 1, 0, 1], :expected => [1.000000, 0.000000, 0.000000, 0.000000, 0.000000, -1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, -2.000000, 0.000000, 1.000000]}].each do |data| 209 | matrix = Geo3d::Matrix.reflection Geo3d::Plane.new(*data[:plane]) 210 | data[:expected].size.should == 16 211 | expected = Geo3d::Matrix.new *data[:expected] 212 | matrix.should == expected 213 | end 214 | end 215 | 216 | it "should have a shadow constructor" do 217 | [{:plane => [0, 1, 0, 1], :light_pos => [0, 700, 0, 1], :expected => [701.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, -1.000000, 0.000000, 0.000000, 701.000000, 0.000000, 0.000000, -700.000000, 0.000000, 700.000000]}].each do |data| 218 | matrix = Geo3d::Matrix.shadow Geo3d::Vector.new(*data[:light_pos]), Geo3d::Plane.new(*data[:plane]) 219 | data[:expected].size.should == 16 220 | expected = Geo3d::Matrix.new *data[:expected] 221 | matrix.should == expected 222 | end 223 | end 224 | 225 | it "multiplying a matrix by the identity matrix should result in the same matrix" do 226 | identity = Geo3d::Matrix.identity 227 | 10.times do 228 | r = random_matrix 229 | (r * identity).should == r 230 | (identity * r).should == r 231 | end 232 | end 233 | 234 | it "should transform vectors" do 235 | 236 | end 237 | 238 | it "should multiply with other matrices" do 239 | 240 | end 241 | end -------------------------------------------------------------------------------- /lib/geo3d/matrix.rb: -------------------------------------------------------------------------------- 1 | module Geo3d 2 | class Matrix 3 | attr_accessor :_11, :_12, :_13, :_14 4 | attr_accessor :_21, :_22, :_23, :_24 5 | attr_accessor :_31, :_32, :_33, :_34 6 | attr_accessor :_41, :_42, :_43, :_44 7 | 8 | def initialize *args 9 | @_11 = 0 10 | @_12 = 0 11 | @_13 = 0 12 | @_14 = 0 13 | @_21 = 0 14 | @_22 = 0 15 | @_23 = 0 16 | @_24 = 0 17 | @_31 = 0 18 | @_32 = 0 19 | @_33 = 0 20 | @_34 = 0 21 | @_41 = 0 22 | @_42 = 0 23 | @_43 = 0 24 | @_44 = 0 25 | @_11 = args[0] if args.size > 0 26 | @_12 = args[1] if args.size > 1 27 | @_13 = args[2] if args.size > 2 28 | @_14 = args[3] if args.size > 3 29 | @_21 = args[4] if args.size > 4 30 | @_22 = args[5] if args.size > 5 31 | @_23 = args[6] if args.size > 6 32 | @_24 = args[7] if args.size > 7 33 | @_31 = args[8] if args.size > 8 34 | @_32 = args[9] if args.size > 9 35 | @_33 = args[10] if args.size > 10 36 | @_34 = args[11] if args.size > 11 37 | @_41 = args[12] if args.size > 12 38 | @_42 = args[13] if args.size > 13 39 | @_43 = args[14] if args.size > 14 40 | @_44 = args[15] if args.size > 15 41 | end 42 | 43 | def to_a 44 | [_11, _12, _13, _14, _21, _22, _23, _24, _31, _32, _33, _34, _41, _42, _43, _44] 45 | end 46 | 47 | def [] x, y 48 | to_a[4*x + y] 49 | end 50 | 51 | def []= x, y, v 52 | send (%w{_11 _12 _13 _14 _21 _22 _23 _24 _31 _32 _33 _34 _41 _42 _43 _44}[4*x + y] + '=').to_sym, v 53 | end 54 | 55 | def row i 56 | if i >= 0 && i <= 3 57 | Vector.new self[0, i], self[1, i], self[2, i], self[3, i] 58 | end 59 | end 60 | 61 | def col i 62 | if i >= 0 && i <= 3 63 | Vector.new self[i, 0], self[i, 1], self [i, 2], self[i, 3] 64 | end 65 | end 66 | 67 | def set_row i, v 68 | self[0, i] = v.x 69 | self[1, i] = v.y 70 | self[2, i] = v.z 71 | self[3, i] = v.w 72 | end 73 | 74 | def set_col i, v 75 | self[i, 0] = v.x 76 | self[i, 1] = v.y 77 | self[i, 2] = v.z 78 | self[i, 3] = v.w 79 | end 80 | 81 | def == m 82 | a = to_a 83 | b = m.to_a 84 | for i in 0..15 85 | return false unless Geo3d::Utils.float_cmp a[i], b[i] 86 | end 87 | true 88 | end 89 | 90 | def != m 91 | !(self == m) 92 | end 93 | 94 | def +@ 95 | self * 1 96 | end 97 | 98 | def -@ 99 | self * -1 100 | end 101 | 102 | def + mat 103 | sum = self.class.new 104 | 105 | sum._11 = _11 + mat._11 106 | sum._12 = _12 + mat._12 107 | sum._13 = _13 + mat._13 108 | sum._14 = _14 + mat._14 109 | 110 | sum._21 = _21 + mat._21 111 | sum._22 = _22 + mat._22 112 | sum._23 = _23 + mat._23 113 | sum._24 = _24 + mat._24 114 | 115 | sum._31 = _31 + mat._31 116 | sum._32 = _32 + mat._32 117 | sum._33 = _33 + mat._33 118 | sum._34 = _34 + mat._34 119 | 120 | sum._41 = _41 + mat._41 121 | sum._42 = _42 + mat._42 122 | sum._43 = _43 + mat._43 123 | sum._44 = _44 + mat._44 124 | 125 | sum 126 | end 127 | 128 | def - mat 129 | sum = self.class.new 130 | 131 | sum._11 = _11 - mat._11 132 | sum._12 = _12 - mat._12 133 | sum._13 = _13 - mat._13 134 | sum._14 = _14 - mat._14 135 | 136 | sum._21 = _21 - mat._21 137 | sum._22 = _22 - mat._22 138 | sum._23 = _23 - mat._23 139 | sum._24 = _24 - mat._24 140 | 141 | sum._31 = _31 - mat._31 142 | sum._32 = _32 - mat._32 143 | sum._33 = _33 - mat._33 144 | sum._34 = _34 - mat._34 145 | 146 | sum._41 = _41 - mat._41 147 | sum._42 = _42 - mat._42 148 | sum._43 = _43 - mat._43 149 | sum._44 = _44 - mat._44 150 | 151 | sum 152 | end 153 | 154 | def * v 155 | result = self.class.new 156 | 157 | if self.class == v.class 158 | matrix = v 159 | 160 | result._11 = _11 * matrix._11 + _12 * matrix._21 + _13 * matrix._31 + _14 * matrix._41 161 | result._12 = _11 * matrix._12 + _12 * matrix._22 + _13 * matrix._32 + _14 * matrix._42 162 | result._13 = _11 * matrix._13 + _12 * matrix._23 + _13 * matrix._33 + _14 * matrix._43 163 | result._14 = _11 * matrix._14 + _12 * matrix._24 + _13 * matrix._34 + _14 * matrix._44 164 | 165 | result._21 = _21 * matrix._11 + _22 * matrix._21 + _23 * matrix._31 + _24 * matrix._41 166 | result._22 = _21 * matrix._12 + _22 * matrix._22 + _23 * matrix._32 + _24 * matrix._42 167 | result._23 = _21 * matrix._13 + _22 * matrix._23 + _23 * matrix._33 + _24 * matrix._43 168 | result._24 = _21 * matrix._14 + _22 * matrix._24 + _23 * matrix._34 + _24 * matrix._44 169 | 170 | result._31 = _31 * matrix._11 + _32 * matrix._21 + _33 * matrix._31 + _34 * matrix._41 171 | result._32 = _31 * matrix._12 + _32 * matrix._22 + _33 * matrix._32 + _34 * matrix._42 172 | result._33 = _31 * matrix._13 + _32 * matrix._23 + _33 * matrix._33 + _34 * matrix._43 173 | result._34 = _31 * matrix._14 + _32 * matrix._24 + _33 * matrix._34 + _34 * matrix._44 174 | 175 | result._41 = _41 * matrix._11 + _42 * matrix._21 + _43 * matrix._31 + _44 * matrix._41 176 | result._42 = _41 * matrix._12 + _42 * matrix._22 + _43 * matrix._32 + _44 * matrix._42 177 | result._43 = _41 * matrix._13 + _42 * matrix._23 + _43 * matrix._33 + _44 * matrix._43 178 | result._44 = _41 * matrix._14 + _42 * matrix._24 + _43 * matrix._34 + _44 * matrix._44 179 | elsif Vector == v.class 180 | vec = v 181 | transformed_vector = Vector.new 182 | transformed_vector.x = _11 * vec.x + _21 * vec.y + _31 * vec.z + _41 * vec.w 183 | transformed_vector.y = _12 * vec.x + _22 * vec.y + _32 * vec.z + _42 * vec.w 184 | transformed_vector.z = _13 * vec.x + _23 * vec.y + _33 * vec.z + _43 * vec.w 185 | transformed_vector.w = _14 * vec.x + _24 * vec.y + _34 * vec.z + _44 * vec.w 186 | return transformed_vector 187 | elsif Triangle == v.class 188 | tri = v 189 | transformed_tri = Triangle.new 190 | transformed_tri.a = self * tri.a 191 | transformed_tri.b = self * tri.b 192 | transformed_tri.c = self * tri.c 193 | return transformed_tri 194 | elsif Array == v.class 195 | return v.map { |i| self * i } 196 | else 197 | scalar = v 198 | result._11 = _11 * scalar 199 | result._12 = _12 * scalar 200 | result._13 = _13 * scalar 201 | result._14 = _14 * scalar 202 | result._21 = _21 * scalar 203 | result._22 = _22 * scalar 204 | result._23 = _23 * scalar 205 | result._24 = _24 * scalar 206 | result._31 = _31 * scalar 207 | result._32 = _32 * scalar 208 | result._33 = _33 * scalar 209 | result._34 = _34 * scalar 210 | result._41 = _41 * scalar 211 | result._42 = _42 * scalar 212 | result._43 = _43 * scalar 213 | result._44 = _44 * scalar 214 | end 215 | 216 | result 217 | end 218 | 219 | def / v 220 | if self.class == v.class 221 | self * v.inverse 222 | elsif Vector == v.class 223 | raise 'dividing matrices by vectors not currently supported' 224 | else 225 | result = self.class.new 226 | scalar = v 227 | result._11 = _11 / scalar 228 | result._12 = _12 / scalar 229 | result._13 = _13 / scalar 230 | result._14 = _14 / scalar 231 | result._21 = _21 / scalar 232 | result._22 = _22 / scalar 233 | result._23 = _23 / scalar 234 | result._24 = _24 / scalar 235 | result._31 = _31 / scalar 236 | result._32 = _32 / scalar 237 | result._33 = _33 / scalar 238 | result._34 = _34 / scalar 239 | result._41 = _41 / scalar 240 | result._42 = _42 / scalar 241 | result._43 = _43 / scalar 242 | result._44 = _44 / scalar 243 | result 244 | end 245 | end 246 | 247 | def transform_coord vec 248 | self * Vector.new(vec.x, vec.y, vec.z, 1.0) 249 | end 250 | 251 | def transform vec 252 | self * vec 253 | end 254 | 255 | def identity? 256 | self == self.class.identity 257 | end 258 | 259 | def translation_component 260 | Vector.new _41, _42, _43 261 | end 262 | 263 | def scaling_component 264 | Vector.new Vector.new(_11, _12, _13).length, Vector.new(_21, _22, _23).length, Vector.new(_31, _32, _33).length 265 | end 266 | 267 | def rotation_component 268 | scaling = scaling_component 269 | return nil if scaling.x.zero? || scaling.y.zero? || scaling.z.zero? 270 | m = Matrix.new 271 | m._11=_11/scaling.x 272 | m._12=_12/scaling.x 273 | m._13=_13/scaling.x 274 | m._21=_21/scaling.y 275 | m._22=_22/scaling.y 276 | m._23=_23/scaling.y 277 | m._31=_31/scaling.z 278 | m._32=_32/scaling.z 279 | m._33=_33/scaling.z 280 | Quaternion.from_matrix m 281 | end 282 | 283 | def self.identity 284 | identity_matrix = self.new 285 | identity_matrix._12 = identity_matrix._13 = identity_matrix._14 = 0 286 | identity_matrix._21 = identity_matrix._23 = identity_matrix._24 = 0 287 | identity_matrix._31 = identity_matrix._32 = identity_matrix._34 = 0 288 | identity_matrix._41 = identity_matrix._42 = identity_matrix._43 = 0 289 | identity_matrix._11 = identity_matrix._22 = identity_matrix._33 = identity_matrix._44 = 1 290 | identity_matrix 291 | end 292 | 293 | def determinant 294 | inverse(true).last 295 | end 296 | 297 | def trace 298 | _11 + _22 + _33 + _44 299 | end 300 | 301 | def orthogonal? 302 | inverse == transpose 303 | end 304 | 305 | def is_rotation_transform? 306 | orthogonal? && Geo3d::Utils.float_cmp(determinant, 1.0) 307 | end 308 | 309 | def inverse with_determinant = false 310 | mat = to_a 311 | dst = Array.new 16 312 | tmp = Array.new 12 313 | src = Array.new 16 314 | 315 | for i in 0..3 316 | src[i] = mat[i*4] 317 | src[i + 4] = mat[i*4 + 1] 318 | src[i + 8] = mat[i*4 + 2] 319 | src[i + 12] = mat[i*4 + 3] 320 | end 321 | 322 | # calculate pairs for first 8 elements (cofactors) 323 | tmp[0] = src[10] * src[15] 324 | tmp[1] = src[11] * src[14] 325 | tmp[2] = src[9] * src[15] 326 | tmp[3] = src[11] * src[13] 327 | tmp[4] = src[9] * src[14] 328 | tmp[5] = src[10] * src[13] 329 | tmp[6] = src[8] * src[15] 330 | tmp[7] = src[11] * src[12] 331 | tmp[8] = src[8] * src[14] 332 | tmp[9] = src[10] * src[12] 333 | tmp[10] = src[8] * src[13] 334 | tmp[11] = src[9] * src[12] 335 | 336 | # calculate first 8 elements (cofactors) 337 | dst[0] = tmp[0]*src[5] + tmp[3]*src[6] + tmp[4]*src[7] 338 | dst[0] -= tmp[1]*src[5] + tmp[2]*src[6] + tmp[5]*src[7] 339 | dst[1] = tmp[1]*src[4] + tmp[6]*src[6] + tmp[9]*src[7] 340 | dst[1] -= tmp[0]*src[4] + tmp[7]*src[6] + tmp[8]*src[7] 341 | dst[2] = tmp[2]*src[4] + tmp[7]*src[5] + tmp[10]*src[7] 342 | dst[2] -= tmp[3]*src[4] + tmp[6]*src[5] + tmp[11]*src[7] 343 | dst[3] = tmp[5]*src[4] + tmp[8]*src[5] + tmp[11]*src[6] 344 | dst[3] -= tmp[4]*src[4] + tmp[9]*src[5] + tmp[10]*src[6] 345 | dst[4] = tmp[1]*src[1] + tmp[2]*src[2] + tmp[5]*src[3] 346 | dst[4] -= tmp[0]*src[1] + tmp[3]*src[2] + tmp[4]*src[3] 347 | dst[5] = tmp[0]*src[0] + tmp[7]*src[2] + tmp[8]*src[3] 348 | dst[5] -= tmp[1]*src[0] + tmp[6]*src[2] + tmp[9]*src[3] 349 | dst[6] = tmp[3]*src[0] + tmp[6]*src[1] + tmp[11]*src[3] 350 | dst[6] -= tmp[2]*src[0] + tmp[7]*src[1] + tmp[10]*src[3] 351 | dst[7] = tmp[4]*src[0] + tmp[9]*src[1] + tmp[10]*src[2] 352 | dst[7] -= tmp[5]*src[0] + tmp[8]*src[1] + tmp[11]*src[2] 353 | 354 | # calculate pairs for second 8 elements (cofactors) 355 | tmp[0] = src[2]*src[7] 356 | tmp[1] = src[3]*src[6] 357 | tmp[2] = src[1]*src[7] 358 | tmp[3] = src[3]*src[5] 359 | tmp[4] = src[1]*src[6] 360 | tmp[5] = src[2]*src[5] 361 | tmp[6] = src[0]*src[7] 362 | tmp[7] = src[3]*src[4] 363 | tmp[8] = src[0]*src[6] 364 | tmp[9] = src[2]*src[4] 365 | tmp[10] = src[0]*src[5] 366 | tmp[11] = src[1]*src[4] 367 | 368 | # calculate second 8 elements (cofactors) 369 | dst[8] = tmp[0]*src[13] + tmp[3]*src[14] + tmp[4]*src[15] 370 | dst[8] -= tmp[1]*src[13] + tmp[2]*src[14] + tmp[5]*src[15] 371 | dst[9] = tmp[1]*src[12] + tmp[6]*src[14] + tmp[9]*src[15] 372 | dst[9] -= tmp[0]*src[12] + tmp[7]*src[14] + tmp[8]*src[15] 373 | dst[10] = tmp[2]*src[12] + tmp[7]*src[13] + tmp[10]*src[15] 374 | dst[10]-= tmp[3]*src[12] + tmp[6]*src[13] + tmp[11]*src[15] 375 | dst[11] = tmp[5]*src[12] + tmp[8]*src[13] + tmp[11]*src[14] 376 | dst[11]-= tmp[4]*src[12] + tmp[9]*src[13] + tmp[10]*src[14] 377 | dst[12] = tmp[2]*src[10] + tmp[5]*src[11] + tmp[1]*src[9] 378 | dst[12]-= tmp[4]*src[11] + tmp[0]*src[9] + tmp[3]*src[10] 379 | dst[13] = tmp[8]*src[11] + tmp[0]*src[8] + tmp[7]*src[10] 380 | dst[13]-= tmp[6]*src[10] + tmp[9]*src[11] + tmp[1]*src[8] 381 | dst[14] = tmp[6]*src[9] + tmp[11]*src[11] + tmp[3]*src[8] 382 | dst[14]-= tmp[10]*src[11] + tmp[2]*src[8] + tmp[7]*src[9] 383 | dst[15] = tmp[10]*src[10] + tmp[4]*src[8] + tmp[9]*src[9] 384 | dst[15]-= tmp[8]*src[9] + tmp[11]*src[10] + tmp[5]*src[8] 385 | 386 | # calculate determinant 387 | det=src[0]*dst[0]+src[1]*dst[1]+src[2]*dst[2]+src[3]*dst[3] 388 | 389 | 390 | # calculate matrix inverse 391 | inverse_det = 1.0/det 392 | for j in 0..15 393 | dst[j] *= inverse_det 394 | end 395 | 396 | inverted_matrix = self.class.new *dst 397 | 398 | if with_determinant 399 | [inverted_matrix, det] 400 | else 401 | inverted_matrix 402 | end 403 | end 404 | 405 | def transpose 406 | transposed_matrix = self.class.new 407 | transposed_matrix._11 = _11 408 | transposed_matrix._12 = _21 409 | transposed_matrix._13 = _31 410 | transposed_matrix._14 = _41 411 | transposed_matrix._21 = _12 412 | transposed_matrix._22 = _22 413 | transposed_matrix._23 = _32 414 | transposed_matrix._24 = _42 415 | transposed_matrix._31 = _13 416 | transposed_matrix._32 = _23 417 | transposed_matrix._33 = _33 418 | transposed_matrix._34 = _43 419 | transposed_matrix._41 = _14 420 | transposed_matrix._42 = _24 421 | transposed_matrix._43 = _34 422 | transposed_matrix._44 = _44 423 | transposed_matrix 424 | end 425 | 426 | def print 427 | puts "_11: #{_11}\n" 428 | puts "_12: #{_12}\n" 429 | puts "_13: #{_13}\n" 430 | puts "_14: #{_14}\n" 431 | puts "_21: #{_21}\n" 432 | puts "_22: #{_22}\n" 433 | puts "_23: #{_23}\n" 434 | puts "_24: #{_24}\n" 435 | puts "_31: #{_31}\n" 436 | puts "_32: #{_32}\n" 437 | puts "_33: #{_33}\n" 438 | puts "_34: #{_34}\n" 439 | puts "_41: #{_41}\n" 440 | puts "_42: #{_42}\n" 441 | puts "_43: #{_43}\n" 442 | puts "_44: #{_44}\n" 443 | end 444 | 445 | def to_s 446 | (0..3).to_a.map { |i| row(i).to_s }.join "\n" 447 | end 448 | 449 | def self.glu_perspective_degrees fovy, aspect, zn, zf 450 | fovy = fovy.to_f 451 | aspect = aspect.to_f 452 | zn = zn.to_f 453 | zf = zf.to_f 454 | range = zn*Math.tan(Geo3d::Utils.to_radians(fovy/2.0)) 455 | self.gl_frustum -range*aspect, range*aspect, -range, range, zn, zf 456 | end 457 | 458 | def self.gl_frustum l, r, bottom, t, zn, zf 459 | l = l.to_f 460 | r = r.to_f 461 | bottom = bottom.to_f 462 | t = t.to_f 463 | zn = zn.to_f 464 | zf = zf.to_f 465 | a = (r+l) / (r-l) 466 | b = (t+bottom) / (t-bottom) 467 | c = -(zf + zn) / (zf - zn) 468 | d = -(2 * zf * zn) / (zf - zn) 469 | matrix = self.new 470 | matrix._11 = 2.0 * zn / (r-l) 471 | matrix._31 = a 472 | matrix._22 = 2.0 * zn / (t-bottom) 473 | matrix._32 = b 474 | matrix._33 = c 475 | matrix._43 = d 476 | matrix._34 = -1.0 477 | matrix 478 | end 479 | 480 | 481 | def self.perspective_fov_rh fovy, aspect, zn, zf 482 | fovy = fovy.to_f 483 | aspect = aspect.to_f 484 | zn = zn.to_f 485 | zf = zf.to_f 486 | y_scale = 1.0 / Math.tan(0.5*fovy) 487 | x_scale = y_scale / aspect 488 | matrix = self.new 489 | matrix._11 = x_scale 490 | matrix._22 = y_scale 491 | matrix._33 = zf/(zn - zf) 492 | matrix._34 = -1 493 | matrix._43 = zn*zf/(zn - zf) 494 | matrix 495 | end 496 | 497 | def self.perspective_fov_lh fovy, aspect, zn, zf 498 | fovy = fovy.to_f 499 | aspect = aspect.to_f 500 | zn = zn.to_f 501 | zf = zf.to_f 502 | y_scale = 1.0 / Math.tan(0.5*fovy) 503 | x_scale = y_scale / aspect 504 | matrix = self.new 505 | matrix._11 = x_scale 506 | matrix._22 = y_scale 507 | matrix._33 = zf/(zf - zn) 508 | matrix._34 = 1 509 | matrix._43 = zn*zf/(zn - zf) 510 | matrix 511 | end 512 | 513 | def self.gl_ortho l, r, b, t, zn, zf 514 | l = l.to_f 515 | r = r.to_f 516 | b = b.to_f 517 | t = t.to_f 518 | zn = zn.to_f 519 | zf = zf.to_f 520 | matrix = self.new 521 | matrix._11 = 2.0 / (r-l) 522 | matrix._22 = 2.0 / (t-b) 523 | matrix._33 = -2.0 / (zf - zn) 524 | matrix._41 = -(r+l) / (r-l) 525 | matrix._42 = -(t+b) / (t-b) 526 | matrix._43 = -(zf+zn) / (zf-zn) 527 | matrix._44 = 1.0 528 | matrix 529 | end 530 | 531 | def self.ortho_off_center_rh l, r, b, t, zn, zf 532 | l = l.to_f 533 | r = r.to_f 534 | b = b.to_f 535 | t = t.to_f 536 | zn = zn.to_f 537 | zf = zf.to_f 538 | m = identity 539 | m._11 = 2.0 / (r - l) 540 | m._22 = 2.0 / (t - b) 541 | m._33 = 1.0 / (zn -zf) 542 | m._41 = -1.0 -2.0 *l / (r - l) 543 | m._42 = 1.0 + 2.0 * t / (b - t) 544 | m._43 = zn / (zn -zf) 545 | m 546 | end 547 | 548 | def self.ortho_off_center_lh l, r, b, t, zn, zf 549 | l = l.to_f 550 | r = r.to_f 551 | b = b.to_f 552 | t = t.to_f 553 | zn = zn.to_f 554 | zf = zf.to_f 555 | m = identity 556 | m._11 = 2.0 / (r - l) 557 | m._22 = 2.0 / (t - b) 558 | m._33 = 1.0 / (zf -zn) 559 | m._41 = -1.0 -2.0 *l / (r - l) 560 | m._42 = 1.0 + 2.0 * t / (b - t) 561 | m._43 = zn / (zn -zf) 562 | m 563 | end 564 | 565 | def self.look_at_rh eye_position, look_at_position, up_direction 566 | zaxis = (eye_position - look_at_position).normalize 567 | xaxis = up_direction.cross(zaxis).normalize 568 | yaxis = zaxis.cross xaxis 569 | 570 | matrix = self.new 571 | 572 | # set column one 573 | matrix._11 = xaxis.x 574 | matrix._21 = xaxis.y 575 | matrix._31 = xaxis.z 576 | matrix._41 = -xaxis.dot(eye_position) 577 | 578 | # set column two 579 | matrix._12 = yaxis.x 580 | matrix._22 = yaxis.y 581 | matrix._32 = yaxis.z 582 | matrix._42 = -yaxis.dot(eye_position) 583 | 584 | # set column three 585 | matrix._13 = zaxis.x 586 | matrix._23 = zaxis.y 587 | matrix._33 = zaxis.z 588 | matrix._43 = -zaxis.dot(eye_position) 589 | 590 | # set column four 591 | matrix._14 = matrix._24 = matrix._34 = 0 592 | matrix._44 = 1 593 | 594 | matrix 595 | end 596 | 597 | def self.look_at_lh eye_position, look_at_position, up_direction 598 | zaxis = (look_at_position - eye_position).normalize 599 | xaxis = up_direction.cross(zaxis).normalize 600 | yaxis = zaxis.cross xaxis 601 | 602 | matrix = self.new 603 | 604 | # set column one 605 | matrix._11 = xaxis.x 606 | matrix._21 = xaxis.y 607 | matrix._31 = xaxis.z 608 | matrix._41 = -xaxis.dot(eye_position) 609 | 610 | # set column two 611 | matrix._12 = yaxis.x 612 | matrix._22 = yaxis.y 613 | matrix._32 = yaxis.z 614 | matrix._42 = -yaxis.dot(eye_position) 615 | 616 | # set column three 617 | matrix._13 = zaxis.x 618 | matrix._23 = zaxis.y 619 | matrix._33 = zaxis.z 620 | matrix._43 = -zaxis.dot(eye_position) 621 | 622 | # set column four 623 | matrix._14 = matrix._24 = matrix._34 = 0 624 | matrix._44 = 1 625 | 626 | matrix 627 | end 628 | 629 | 630 | def self.viewport x, y, width, height 631 | self.scaling(width.to_f / 2.0, height.to_f / 2.0, 0.5) * self.translation(x.to_f + width.to_f / 2.0, y.to_f + height.to_f / 2.0, 0.5) 632 | end 633 | 634 | def self.reflection reflection_plane 635 | reflection_plane = Geo3d::Vector.new *reflection_plane.to_a 636 | reflection_matrix = self.new 637 | 638 | plane_magnitude = Vector.new(reflection_plane.x, reflection_plane.y, reflection_plane.z, 0).length 639 | normalized_plane = reflection_plane / plane_magnitude 640 | 641 | # row one 642 | reflection_matrix._11 = -2 * normalized_plane.x * normalized_plane.x + 1 643 | reflection_matrix._12 = -2 * normalized_plane.y * normalized_plane.x 644 | reflection_matrix._13 = -2 * normalized_plane.z * normalized_plane.x 645 | reflection_matrix._14 = 0 646 | 647 | # row two 648 | reflection_matrix._21 = -2 * normalized_plane.x * normalized_plane.y 649 | reflection_matrix._22 = -2 * normalized_plane.y * normalized_plane.y + 1 650 | reflection_matrix._23 = -2 * normalized_plane.z * normalized_plane.y 651 | reflection_matrix._24 = 0 652 | 653 | # row three 654 | reflection_matrix._31 = -2 * normalized_plane.x * normalized_plane.z 655 | reflection_matrix._32 = -2 * normalized_plane.y * normalized_plane.z 656 | reflection_matrix._33 = -2 * normalized_plane.z * normalized_plane.z + 1 657 | reflection_matrix._34 = 0 658 | 659 | # row four 660 | reflection_matrix._41 = -2 * normalized_plane.x * normalized_plane.w 661 | reflection_matrix._42 = -2 * normalized_plane.y * normalized_plane.w 662 | reflection_matrix._43 = -2 * normalized_plane.z * normalized_plane.w 663 | reflection_matrix._44 = 1 664 | 665 | reflection_matrix 666 | end 667 | 668 | def self.shadow light_position, plane 669 | plane = Geo3d::Vector.new *plane.to_a 670 | norm = plane.x * plane.x + plane.y * plane.y + plane.z * plane.z 671 | normalized_plane = plane / norm 672 | dot = normalized_plane.dot(light_position) 673 | 674 | m = self.new 675 | m._11 = dot - normalized_plane.a * light_position.x 676 | m._12 = -normalized_plane.a * light_position.y 677 | m._13 = -normalized_plane.a * light_position.z 678 | m._14 = -normalized_plane.a * light_position.w 679 | m._21 = -normalized_plane.b * light_position.x 680 | m._22 = dot - normalized_plane.b * light_position.y 681 | m._23 = -normalized_plane.b * light_position.z 682 | m._24 = -normalized_plane.b * light_position.w 683 | m._31 = -normalized_plane.c * light_position.x 684 | m._32 = -normalized_plane.c * light_position.y 685 | m._33 = dot - normalized_plane.c * light_position.z 686 | m._34 = -normalized_plane.c * light_position.w 687 | m._41 = -normalized_plane.d * light_position.x 688 | m._42 = -normalized_plane.d * light_position.y 689 | m._43 = -normalized_plane.d * light_position.z 690 | m._44 = dot - normalized_plane.d * light_position.w 691 | 692 | m 693 | end 694 | 695 | 696 | def self.translation x, y, z 697 | translation_matrix = self.new 698 | translation_matrix._11 = translation_matrix._22 = translation_matrix._33 = translation_matrix._44 = 1.0 699 | #todo: consider simplifying with identity 700 | translation_matrix._41 = x.to_f 701 | translation_matrix._42 = y.to_f 702 | translation_matrix._43 = z.to_f 703 | translation_matrix 704 | end 705 | 706 | def self.scaling x, y, z 707 | scaling_matrix = self.new 708 | scaling_matrix._11 = x.to_f 709 | scaling_matrix._22 = y.to_f 710 | scaling_matrix._33 = z.to_f 711 | scaling_matrix._44 = 1.0 712 | scaling_matrix 713 | end 714 | 715 | def self.uniform_scaling scale 716 | scaling scale, scale, scale 717 | end 718 | 719 | def self.rotation_x angle 720 | m = identity 721 | sine = Math.sin angle 722 | cosine = Math.cos angle 723 | m._22 = cosine 724 | m._33 = cosine 725 | m._23 = sine 726 | m._32 = -sine 727 | m 728 | end 729 | 730 | def self.rotation_y angle 731 | m = identity 732 | sine = Math.sin angle 733 | cosine = Math.cos angle 734 | m._11 = cosine 735 | m._33 = cosine 736 | m._13 = -sine 737 | m._31 = sine 738 | m 739 | end 740 | 741 | def self.rotation_z angle 742 | m = identity 743 | sine = Math.sin angle 744 | cosine = Math.cos angle 745 | m._11 = cosine 746 | m._22 = cosine 747 | m._12 = sine 748 | m._21 = -sine 749 | m 750 | end 751 | 752 | def self.rotation axis, angle 753 | v = axis.normalize 754 | m = identity 755 | m._11 = (1.0 - Math.cos(angle)) * v.x * v.x + Math.cos(angle) 756 | m._21 = (1.0 - Math.cos(angle)) * v.x * v.y - Math.sin(angle) * v.z 757 | m._31 = (1.0 - Math.cos(angle)) * v.x * v.z + Math.sin(angle) * v.y 758 | m._12 = (1.0 - Math.cos(angle)) * v.y * v.x + Math.sin(angle) * v.z 759 | m._22 = (1.0 - Math.cos(angle)) * v.y * v.y + Math.cos(angle) 760 | m._32 = (1.0 - Math.cos(angle)) * v.y * v.z - Math.sin(angle) * v.x 761 | m._13 = (1.0 - Math.cos(angle)) * v.z * v.x - Math.sin(angle) * v.y 762 | m._23 = (1.0 - Math.cos(angle)) * v.z * v.y + Math.sin(angle) * v.x 763 | m._33 = (1.0 - Math.cos(angle)) * v.z * v.z + Math.cos(angle) 764 | m 765 | end 766 | end 767 | end --------------------------------------------------------------------------------