bullet3 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/data/cube.urdf +22 -0
- data/data/plane.urdf +19 -0
- data/ext/bullet3/CMakeLists.txt +224 -0
- data/ext/bullet3/bullet3.cpp +34 -0
- data/ext/bullet3/collision/rb_collision.cpp +428 -0
- data/ext/bullet3/collision/rb_collision.hpp +87 -0
- data/ext/bullet3/collision/shapes/rb_collision_shape.cpp +485 -0
- data/ext/bullet3/collision/shapes/rb_collision_shape.hpp +5 -0
- data/ext/bullet3/constraints/rb_constraints.cpp +1310 -0
- data/ext/bullet3/constraints/rb_constraints.hpp +226 -0
- data/ext/bullet3/dynamics/rb_dynamics.cpp +862 -0
- data/ext/bullet3/dynamics/rb_dynamics.hpp +180 -0
- data/ext/bullet3/extconf.rb +57 -0
- data/ext/bullet3/io/rb_io.cpp +108 -0
- data/ext/bullet3/io/rb_io.hpp +40 -0
- data/ext/bullet3/linear_math/rb_matrix3x3.cpp +93 -0
- data/ext/bullet3/linear_math/rb_matrix3x3.hpp +5 -0
- data/ext/bullet3/linear_math/rb_quaternion.cpp +134 -0
- data/ext/bullet3/linear_math/rb_quaternion.hpp +5 -0
- data/ext/bullet3/linear_math/rb_transform.cpp +157 -0
- data/ext/bullet3/linear_math/rb_transform.hpp +34 -0
- data/ext/bullet3/linear_math/rb_vector3.cpp +123 -0
- data/ext/bullet3/linear_math/rb_vector3.hpp +5 -0
- data/ext/bullet3/multi_body/rb_multi_body.cpp +1000 -0
- data/ext/bullet3/multi_body/rb_multi_body.hpp +190 -0
- data/ext/bullet3/soft_body/rb_soft_body.cpp +720 -0
- data/ext/bullet3/soft_body/rb_soft_body.hpp +122 -0
- data/ext/bullet3/util/rb_debug_draw.cpp +250 -0
- data/ext/bullet3/util/rb_debug_draw.hpp +47 -0
- data/ext/bullet3/util/ruby_cpp_compat.hpp +29 -0
- data/ext/bullet3/util/type_conversions.hpp +107 -0
- data/ext/bullet3/vehicle/rb_vehicle.cpp +467 -0
- data/ext/bullet3/vehicle/rb_vehicle.hpp +115 -0
- data/lib/bullet3/collision.rb +6 -0
- data/lib/bullet3/constraints.rb +6 -0
- data/lib/bullet3/data.rb +39 -0
- data/lib/bullet3/debug_draw.rb +73 -0
- data/lib/bullet3/dynamics.rb +4 -0
- data/lib/bullet3/io.rb +110 -0
- data/lib/bullet3/linear_math/matrix3x3.rb +69 -0
- data/lib/bullet3/linear_math/quaternion.rb +187 -0
- data/lib/bullet3/linear_math/transform.rb +48 -0
- data/lib/bullet3/linear_math/vector3.rb +199 -0
- data/lib/bullet3/linear_math.rb +6 -0
- data/lib/bullet3/multi_body.rb +6 -0
- data/lib/bullet3/simulation.rb +1170 -0
- data/lib/bullet3/soft_body.rb +6 -0
- data/lib/bullet3/vehicle.rb +4 -0
- data/lib/bullet3/version.rb +5 -0
- data/lib/bullet3.rb +49 -0
- metadata +124 -0
data/lib/bullet3/io.rb
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bullet3
|
|
4
|
+
module IO
|
|
5
|
+
class Serializer
|
|
6
|
+
def initialize(simulation = nil)
|
|
7
|
+
@simulation = simulation
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def serialize_world(simulation_or_world = nil)
|
|
11
|
+
target = simulation_or_world || @simulation
|
|
12
|
+
raise ArgumentError, "a simulation or dynamics world is required" unless target
|
|
13
|
+
|
|
14
|
+
if native_serializer
|
|
15
|
+
return native_serializer.serialize_world(extract_world(target))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raise Bullet3::Error, "native extension is required for .bullet serialization"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save_world(simulation_or_world, filename)
|
|
22
|
+
if bullet_file?(filename)
|
|
23
|
+
return save_bullet(simulation_or_world, filename)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
simulation = extract_simulation(simulation_or_world)
|
|
27
|
+
simulation.save_world(filename)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_world(simulation, filename)
|
|
31
|
+
if bullet_file?(filename)
|
|
32
|
+
return load_bullet(simulation, filename)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
extract_simulation(simulation).load_world(filename)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def save_bullet(simulation_or_world, filename)
|
|
39
|
+
raise Bullet3::Error, "native extension is required for .bullet serialization" unless native_serializer
|
|
40
|
+
|
|
41
|
+
native_serializer.save_world(extract_world(simulation_or_world), filename)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load_bullet(simulation, filename)
|
|
45
|
+
extract_simulation(simulation).load_bullet(filename)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def native_serializer
|
|
51
|
+
return nil unless IO.const_defined?(:DefaultSerializer, false)
|
|
52
|
+
|
|
53
|
+
@native_serializer ||= DefaultSerializer.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_simulation(value)
|
|
57
|
+
return value if value.is_a?(Simulation)
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "expected Bullet3::Simulation"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def extract_world(value)
|
|
63
|
+
return value.world if value.is_a?(Simulation)
|
|
64
|
+
return value if value.is_a?(DiscreteDynamicsWorld)
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "expected Bullet3::Simulation or Bullet3::DiscreteDynamicsWorld"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def bullet_file?(filename)
|
|
70
|
+
File.extname(filename.to_s) == ".bullet"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class URDFImporter
|
|
75
|
+
attr_reader :simulation
|
|
76
|
+
|
|
77
|
+
def initialize(simulation = Simulation.new)
|
|
78
|
+
@simulation = simulation
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def load(filename, **options)
|
|
82
|
+
simulation.load_urdf(filename, **options)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class SDFImporter
|
|
87
|
+
attr_reader :simulation
|
|
88
|
+
|
|
89
|
+
def initialize(simulation = Simulation.new)
|
|
90
|
+
@simulation = simulation
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def load(filename, **options)
|
|
94
|
+
simulation.load_sdf(filename, **options)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class MJCFImporter
|
|
99
|
+
attr_reader :simulation
|
|
100
|
+
|
|
101
|
+
def initialize(simulation = Simulation.new)
|
|
102
|
+
@simulation = simulation
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load(filename, **options)
|
|
106
|
+
simulation.load_mjcf(filename, **options)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bullet3
|
|
4
|
+
class Matrix3x3
|
|
5
|
+
attr_reader :rows
|
|
6
|
+
|
|
7
|
+
def self.identity
|
|
8
|
+
new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.from_quaternion(quaternion)
|
|
12
|
+
q = Quaternion.coerce(quaternion).normalized
|
|
13
|
+
xx = q.x * q.x
|
|
14
|
+
yy = q.y * q.y
|
|
15
|
+
zz = q.z * q.z
|
|
16
|
+
xy = q.x * q.y
|
|
17
|
+
xz = q.x * q.z
|
|
18
|
+
yz = q.y * q.z
|
|
19
|
+
wx = q.w * q.x
|
|
20
|
+
wy = q.w * q.y
|
|
21
|
+
wz = q.w * q.z
|
|
22
|
+
|
|
23
|
+
new([
|
|
24
|
+
[1.0 - (2.0 * (yy + zz)), 2.0 * (xy - wz), 2.0 * (xz + wy)],
|
|
25
|
+
[2.0 * (xy + wz), 1.0 - (2.0 * (xx + zz)), 2.0 * (yz - wx)],
|
|
26
|
+
[2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - (2.0 * (xx + yy))]
|
|
27
|
+
])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(*values)
|
|
31
|
+
rows = if values.empty?
|
|
32
|
+
self.class.identity.rows
|
|
33
|
+
elsif values.length == 1
|
|
34
|
+
values.first
|
|
35
|
+
elsif values.length == 9
|
|
36
|
+
values.each_slice(3).to_a
|
|
37
|
+
else
|
|
38
|
+
raise ArgumentError, "expected a 3x3 row array or 9 scalar values"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@rows = rows.map { |row| row.map { |value| Float(value) } }
|
|
42
|
+
raise ArgumentError, "matrix must be 3x3" unless @rows.length == 3 && @rows.all? { |row| row.length == 3 }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def *(vector)
|
|
46
|
+
vector = Vector3.coerce(vector)
|
|
47
|
+
Vector3.new(
|
|
48
|
+
dot_row(0, vector),
|
|
49
|
+
dot_row(1, vector),
|
|
50
|
+
dot_row(2, vector)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def transpose
|
|
55
|
+
self.class.new(rows.transpose)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_a
|
|
59
|
+
rows.map(&:dup)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def dot_row(index, vector)
|
|
65
|
+
row = rows[index]
|
|
66
|
+
(row[0] * vector.x) + (row[1] * vector.y) + (row[2] * vector.z)
|
|
67
|
+
end
|
|
68
|
+
end unless const_defined?(:Matrix3x3, false)
|
|
69
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bullet3
|
|
4
|
+
class Quaternion
|
|
5
|
+
EPSILON = 1e-6
|
|
6
|
+
|
|
7
|
+
attr_accessor :x, :y, :z, :w
|
|
8
|
+
|
|
9
|
+
def self.identity
|
|
10
|
+
new(0.0, 0.0, 0.0, 1.0)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_axis_angle(axis, angle)
|
|
14
|
+
axis = Vector3.coerce(axis).normalized
|
|
15
|
+
half_angle = Float(angle) / 2.0
|
|
16
|
+
scale = Math.sin(half_angle)
|
|
17
|
+
|
|
18
|
+
new(axis.x * scale, axis.y * scale, axis.z * scale, Math.cos(half_angle))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_euler(roll, pitch, yaw)
|
|
22
|
+
cr = Math.cos(Float(roll) / 2.0)
|
|
23
|
+
sr = Math.sin(Float(roll) / 2.0)
|
|
24
|
+
cp = Math.cos(Float(pitch) / 2.0)
|
|
25
|
+
sp = Math.sin(Float(pitch) / 2.0)
|
|
26
|
+
cy = Math.cos(Float(yaw) / 2.0)
|
|
27
|
+
sy = Math.sin(Float(yaw) / 2.0)
|
|
28
|
+
|
|
29
|
+
new(
|
|
30
|
+
(sr * cp * cy) - (cr * sp * sy),
|
|
31
|
+
(cr * sp * cy) + (sr * cp * sy),
|
|
32
|
+
(cr * cp * sy) - (sr * sp * cy),
|
|
33
|
+
(cr * cp * cy) + (sr * sp * sy)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(x = 0.0, y = 0.0, z = 0.0, w = 1.0)
|
|
38
|
+
@x = Float(x)
|
|
39
|
+
@y = Float(y)
|
|
40
|
+
@z = Float(z)
|
|
41
|
+
@w = Float(w)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def *(other)
|
|
45
|
+
other = self.class.coerce(other)
|
|
46
|
+
self.class.new(
|
|
47
|
+
(w * other.x) + (x * other.w) + (y * other.z) - (z * other.y),
|
|
48
|
+
(w * other.y) - (x * other.z) + (y * other.w) + (z * other.x),
|
|
49
|
+
(w * other.z) + (x * other.y) - (y * other.x) + (z * other.w),
|
|
50
|
+
(w * other.w) - (x * other.x) - (y * other.y) - (z * other.z)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ==(other)
|
|
55
|
+
other = self.class.try_coerce(other)
|
|
56
|
+
return false unless other
|
|
57
|
+
|
|
58
|
+
to_a.zip(other.to_a).all? { |left, right| (left - right).abs <= EPSILON }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def inverse
|
|
62
|
+
magnitude = length2
|
|
63
|
+
raise ZeroDivisionError, "cannot invert a zero-length quaternion" if magnitude <= EPSILON
|
|
64
|
+
|
|
65
|
+
self.class.new(-x / magnitude, -y / magnitude, -z / magnitude, w / magnitude)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def angle
|
|
69
|
+
2.0 * Math.acos(w.clamp(-1.0, 1.0))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def axis
|
|
73
|
+
scale = Math.sqrt(1.0 - (w * w))
|
|
74
|
+
return Vector3.new(1.0, 0.0, 0.0) if scale <= EPSILON
|
|
75
|
+
|
|
76
|
+
Vector3.new(x / scale, y / scale, z / scale)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def slerp(other, t)
|
|
80
|
+
other = self.class.coerce(other)
|
|
81
|
+
t = Float(t)
|
|
82
|
+
cosine = dot(other)
|
|
83
|
+
|
|
84
|
+
if cosine.negative?
|
|
85
|
+
other = self.class.new(-other.x, -other.y, -other.z, -other.w)
|
|
86
|
+
cosine = -cosine
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return linear_interpolate(other, t).normalized if cosine > 0.9995
|
|
90
|
+
|
|
91
|
+
theta = Math.acos(cosine.clamp(-1.0, 1.0))
|
|
92
|
+
sin_theta = Math.sin(theta)
|
|
93
|
+
left = Math.sin((1.0 - t) * theta) / sin_theta
|
|
94
|
+
right = Math.sin(t * theta) / sin_theta
|
|
95
|
+
|
|
96
|
+
self.class.new(
|
|
97
|
+
(x * left) + (other.x * right),
|
|
98
|
+
(y * left) + (other.y * right),
|
|
99
|
+
(z * left) + (other.z * right),
|
|
100
|
+
(w * left) + (other.w * right)
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_euler
|
|
105
|
+
roll = Math.atan2(2.0 * ((w * x) + (y * z)), 1.0 - (2.0 * ((x * x) + (y * y))))
|
|
106
|
+
pitch_sin = 2.0 * ((w * y) - (z * x))
|
|
107
|
+
pitch = if pitch_sin.abs >= 1.0
|
|
108
|
+
pitch_sin.negative? ? -Math::PI / 2.0 : Math::PI / 2.0
|
|
109
|
+
else
|
|
110
|
+
Math.asin(pitch_sin)
|
|
111
|
+
end
|
|
112
|
+
yaw = Math.atan2(2.0 * ((w * z) + (x * y)), 1.0 - (2.0 * ((y * y) + (z * z))))
|
|
113
|
+
|
|
114
|
+
[roll, pitch, yaw]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_matrix
|
|
118
|
+
Matrix3x3.from_quaternion(self)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def normalized
|
|
122
|
+
magnitude = Math.sqrt(length2)
|
|
123
|
+
return self.class.identity if magnitude <= EPSILON
|
|
124
|
+
|
|
125
|
+
self.class.new(x / magnitude, y / magnitude, z / magnitude, w / magnitude)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def to_a
|
|
129
|
+
[x, y, z, w]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.coerce(value)
|
|
133
|
+
coerced = try_coerce(value)
|
|
134
|
+
return coerced if coerced
|
|
135
|
+
|
|
136
|
+
raise TypeError, "expected Bullet3::Quaternion or a 4-element Array"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.try_coerce(value)
|
|
140
|
+
return value if value.is_a?(self)
|
|
141
|
+
|
|
142
|
+
return new(*value) if value.respond_to?(:to_ary) && value.to_ary.length == 4
|
|
143
|
+
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
protected
|
|
148
|
+
|
|
149
|
+
def dot(other)
|
|
150
|
+
(x * other.x) + (y * other.y) + (z * other.z) + (w * other.w)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def length2
|
|
156
|
+
dot(self)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def linear_interpolate(other, t)
|
|
160
|
+
self.class.new(
|
|
161
|
+
x + ((other.x - x) * t),
|
|
162
|
+
y + ((other.y - y) * t),
|
|
163
|
+
z + ((other.z - z) * t),
|
|
164
|
+
w + ((other.w - w) * t)
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end unless const_defined?(:Quaternion, false)
|
|
168
|
+
|
|
169
|
+
class Quaternion
|
|
170
|
+
EPSILON = 1e-6 unless const_defined?(:EPSILON, false)
|
|
171
|
+
|
|
172
|
+
def self.coerce(value)
|
|
173
|
+
coerced = try_coerce(value)
|
|
174
|
+
return coerced if coerced
|
|
175
|
+
|
|
176
|
+
raise TypeError, "expected Bullet3::Quaternion or a 4-element Array"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.try_coerce(value)
|
|
180
|
+
return value if value.is_a?(self)
|
|
181
|
+
|
|
182
|
+
return new(*value) if value.respond_to?(:to_ary) && value.to_ary.length == 4
|
|
183
|
+
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bullet3
|
|
4
|
+
class Transform
|
|
5
|
+
attr_accessor :origin, :rotation
|
|
6
|
+
|
|
7
|
+
def self.identity
|
|
8
|
+
new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(rotation = Quaternion.identity, origin = Vector3.zero)
|
|
12
|
+
@rotation = Quaternion.coerce(rotation)
|
|
13
|
+
@origin = Vector3.coerce(origin)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def basis
|
|
17
|
+
rotation.to_matrix
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def *(other)
|
|
21
|
+
case other
|
|
22
|
+
when Transform
|
|
23
|
+
self.class.new(rotation * other.rotation, transform_vector(other.origin))
|
|
24
|
+
else
|
|
25
|
+
transform_vector(other)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inverse
|
|
30
|
+
inverse_rotation = rotation.inverse.normalized
|
|
31
|
+
self.class.new(inverse_rotation, inverse_rotation.to_matrix * -origin)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inverse_times(other)
|
|
35
|
+
inverse * other
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_a
|
|
39
|
+
[origin.to_a, rotation.to_a]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def transform_vector(vector)
|
|
45
|
+
(basis * vector) + origin
|
|
46
|
+
end
|
|
47
|
+
end unless const_defined?(:Transform, false)
|
|
48
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bullet3
|
|
4
|
+
class Vector3
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
EPSILON = 1e-6
|
|
8
|
+
COMPONENTS = { 0 => :x, 1 => :y, 2 => :z }.freeze
|
|
9
|
+
|
|
10
|
+
attr_accessor :x, :y, :z
|
|
11
|
+
|
|
12
|
+
def self.zero
|
|
13
|
+
new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(x = 0.0, y = 0.0, z = 0.0)
|
|
17
|
+
@x = Float(x)
|
|
18
|
+
@y = Float(y)
|
|
19
|
+
@z = Float(z)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def [](index)
|
|
23
|
+
public_send(component_name(index))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def []=(index, value)
|
|
27
|
+
public_send("#{component_name(index)}=", Float(value))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def each(&block)
|
|
31
|
+
to_a.each(&block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def +(other)
|
|
35
|
+
other = self.class.coerce(other)
|
|
36
|
+
self.class.new(x + other.x, y + other.y, z + other.z)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def -(other)
|
|
40
|
+
other = self.class.coerce(other)
|
|
41
|
+
self.class.new(x - other.x, y - other.y, z - other.z)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def -@
|
|
45
|
+
self.class.new(-x, -y, -z)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def *(scalar)
|
|
49
|
+
self.class.new(x * Float(scalar), y * Float(scalar), z * Float(scalar))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def /(scalar)
|
|
53
|
+
scalar = Float(scalar)
|
|
54
|
+
raise ZeroDivisionError, "divided by 0" if scalar.zero?
|
|
55
|
+
|
|
56
|
+
self.class.new(x / scalar, y / scalar, z / scalar)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ==(other)
|
|
60
|
+
other = self.class.try_coerce(other)
|
|
61
|
+
return false unless other
|
|
62
|
+
|
|
63
|
+
distance2(other) <= EPSILON * EPSILON
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def length
|
|
67
|
+
Math.sqrt(length2)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def length2
|
|
71
|
+
dot(self)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def normalize
|
|
75
|
+
normalized = self.normalized
|
|
76
|
+
self.x = normalized.x
|
|
77
|
+
self.y = normalized.y
|
|
78
|
+
self.z = normalized.z
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias normalize! normalize
|
|
83
|
+
|
|
84
|
+
def normalized
|
|
85
|
+
magnitude = length
|
|
86
|
+
return self.class.zero if magnitude <= EPSILON
|
|
87
|
+
|
|
88
|
+
self / magnitude
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def dot(other)
|
|
92
|
+
other = self.class.coerce(other)
|
|
93
|
+
(x * other.x) + (y * other.y) + (z * other.z)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def cross(other)
|
|
97
|
+
other = self.class.coerce(other)
|
|
98
|
+
self.class.new(
|
|
99
|
+
(y * other.z) - (z * other.y),
|
|
100
|
+
(z * other.x) - (x * other.z),
|
|
101
|
+
(x * other.y) - (y * other.x)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def distance(other)
|
|
106
|
+
Math.sqrt(distance2(other))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def distance2(other)
|
|
110
|
+
(self - other).length2
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def lerp(other, t)
|
|
114
|
+
other = self.class.coerce(other)
|
|
115
|
+
self + ((other - self) * Float(t))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def angle(other)
|
|
119
|
+
other = self.class.coerce(other)
|
|
120
|
+
denominator = length * other.length
|
|
121
|
+
return 0.0 if denominator <= EPSILON
|
|
122
|
+
|
|
123
|
+
cosine = dot(other) / denominator
|
|
124
|
+
Math.acos(cosine.clamp(-1.0, 1.0))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def absolute
|
|
128
|
+
self.class.new(x.abs, y.abs, z.abs)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_a
|
|
132
|
+
[x, y, z]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def to_s
|
|
136
|
+
"(#{x}, #{y}, #{z})"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def inspect
|
|
140
|
+
"#<Bullet3::Vector3 (#{x}, #{y}, #{z})>"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.coerce(value)
|
|
144
|
+
coerced = try_coerce(value)
|
|
145
|
+
return coerced if coerced
|
|
146
|
+
|
|
147
|
+
raise TypeError, "expected Bullet3::Vector3 or a 3-element Array"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.try_coerce(value)
|
|
151
|
+
return value if value.is_a?(self)
|
|
152
|
+
|
|
153
|
+
return new(*value) if value.respond_to?(:to_ary) && value.to_ary.length == 3
|
|
154
|
+
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def component_name(index)
|
|
161
|
+
COMPONENTS.fetch(Integer(index))
|
|
162
|
+
rescue KeyError
|
|
163
|
+
raise IndexError, "index #{index} outside vector"
|
|
164
|
+
end
|
|
165
|
+
end unless const_defined?(:Vector3, false)
|
|
166
|
+
|
|
167
|
+
class Vector3
|
|
168
|
+
EPSILON = 1e-6 unless const_defined?(:EPSILON, false)
|
|
169
|
+
|
|
170
|
+
include Enumerable unless include?(Enumerable)
|
|
171
|
+
|
|
172
|
+
def each(&block)
|
|
173
|
+
to_a.each(&block)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def to_s
|
|
177
|
+
"(#{x}, #{y}, #{z})"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def inspect
|
|
181
|
+
"#<Bullet3::Vector3 (#{x}, #{y}, #{z})>"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def self.coerce(value)
|
|
185
|
+
coerced = try_coerce(value)
|
|
186
|
+
return coerced if coerced
|
|
187
|
+
|
|
188
|
+
raise TypeError, "expected Bullet3::Vector3 or a 3-element Array"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.try_coerce(value)
|
|
192
|
+
return value if value.is_a?(self)
|
|
193
|
+
|
|
194
|
+
return new(*value) if value.respond_to?(:to_ary) && value.to_ary.length == 3
|
|
195
|
+
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|