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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +194 -0
  4. data/data/cube.urdf +22 -0
  5. data/data/plane.urdf +19 -0
  6. data/ext/bullet3/CMakeLists.txt +224 -0
  7. data/ext/bullet3/bullet3.cpp +34 -0
  8. data/ext/bullet3/collision/rb_collision.cpp +428 -0
  9. data/ext/bullet3/collision/rb_collision.hpp +87 -0
  10. data/ext/bullet3/collision/shapes/rb_collision_shape.cpp +485 -0
  11. data/ext/bullet3/collision/shapes/rb_collision_shape.hpp +5 -0
  12. data/ext/bullet3/constraints/rb_constraints.cpp +1310 -0
  13. data/ext/bullet3/constraints/rb_constraints.hpp +226 -0
  14. data/ext/bullet3/dynamics/rb_dynamics.cpp +862 -0
  15. data/ext/bullet3/dynamics/rb_dynamics.hpp +180 -0
  16. data/ext/bullet3/extconf.rb +57 -0
  17. data/ext/bullet3/io/rb_io.cpp +108 -0
  18. data/ext/bullet3/io/rb_io.hpp +40 -0
  19. data/ext/bullet3/linear_math/rb_matrix3x3.cpp +93 -0
  20. data/ext/bullet3/linear_math/rb_matrix3x3.hpp +5 -0
  21. data/ext/bullet3/linear_math/rb_quaternion.cpp +134 -0
  22. data/ext/bullet3/linear_math/rb_quaternion.hpp +5 -0
  23. data/ext/bullet3/linear_math/rb_transform.cpp +157 -0
  24. data/ext/bullet3/linear_math/rb_transform.hpp +34 -0
  25. data/ext/bullet3/linear_math/rb_vector3.cpp +123 -0
  26. data/ext/bullet3/linear_math/rb_vector3.hpp +5 -0
  27. data/ext/bullet3/multi_body/rb_multi_body.cpp +1000 -0
  28. data/ext/bullet3/multi_body/rb_multi_body.hpp +190 -0
  29. data/ext/bullet3/soft_body/rb_soft_body.cpp +720 -0
  30. data/ext/bullet3/soft_body/rb_soft_body.hpp +122 -0
  31. data/ext/bullet3/util/rb_debug_draw.cpp +250 -0
  32. data/ext/bullet3/util/rb_debug_draw.hpp +47 -0
  33. data/ext/bullet3/util/ruby_cpp_compat.hpp +29 -0
  34. data/ext/bullet3/util/type_conversions.hpp +107 -0
  35. data/ext/bullet3/vehicle/rb_vehicle.cpp +467 -0
  36. data/ext/bullet3/vehicle/rb_vehicle.hpp +115 -0
  37. data/lib/bullet3/collision.rb +6 -0
  38. data/lib/bullet3/constraints.rb +6 -0
  39. data/lib/bullet3/data.rb +39 -0
  40. data/lib/bullet3/debug_draw.rb +73 -0
  41. data/lib/bullet3/dynamics.rb +4 -0
  42. data/lib/bullet3/io.rb +110 -0
  43. data/lib/bullet3/linear_math/matrix3x3.rb +69 -0
  44. data/lib/bullet3/linear_math/quaternion.rb +187 -0
  45. data/lib/bullet3/linear_math/transform.rb +48 -0
  46. data/lib/bullet3/linear_math/vector3.rb +199 -0
  47. data/lib/bullet3/linear_math.rb +6 -0
  48. data/lib/bullet3/multi_body.rb +6 -0
  49. data/lib/bullet3/simulation.rb +1170 -0
  50. data/lib/bullet3/soft_body.rb +6 -0
  51. data/lib/bullet3/vehicle.rb +4 -0
  52. data/lib/bullet3/version.rb +5 -0
  53. data/lib/bullet3.rb +49 -0
  54. metadata +124 -0
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet3
4
+ end
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "linear_math/vector3"
4
+ require_relative "linear_math/quaternion"
5
+ require_relative "linear_math/matrix3x3"
6
+ require_relative "linear_math/transform"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet3
4
+ module MultiBody
5
+ end unless const_defined?(:MultiBody, false)
6
+ end