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
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rexml/document"
|
|
5
|
+
|
|
6
|
+
module Bullet3
|
|
7
|
+
class Simulation
|
|
8
|
+
attr_reader :world, :mode
|
|
9
|
+
|
|
10
|
+
def initialize(mode: :direct)
|
|
11
|
+
mode = mode.to_sym
|
|
12
|
+
raise ArgumentError, "unsupported connection mode: #{mode}" unless %i[direct gui shared_memory].include?(mode)
|
|
13
|
+
Bullet3.require_native_extension!("Bullet3::Simulation") unless Bullet3.native?
|
|
14
|
+
|
|
15
|
+
@mode = mode
|
|
16
|
+
@world = DiscreteDynamicsWorld.create
|
|
17
|
+
@shapes = []
|
|
18
|
+
@shape_specs = []
|
|
19
|
+
@bodies = []
|
|
20
|
+
@body_specs = []
|
|
21
|
+
@constraints = []
|
|
22
|
+
@constraint_specs = []
|
|
23
|
+
@models = {}
|
|
24
|
+
@importers = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set_gravity(x, y = nil, z = nil)
|
|
28
|
+
vector = vector_argument(x, y, z)
|
|
29
|
+
world.gravity = vector
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def step_simulation(time_step: 1.0 / 240.0, max_sub_steps: 1, fixed_time_step: 1.0 / 240.0)
|
|
33
|
+
world.step_simulation(time_step, max_sub_steps, fixed_time_step)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_collision_shape(type, **options)
|
|
37
|
+
shape = build_collision_shape(type.to_sym, options)
|
|
38
|
+
register_shape(shape, type: type.to_sym, options: serializable_options(options))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_rigid_body(mass:, collision_shape:, position: [0, 0, 0], orientation: Quaternion.identity, rgba_color: [220, 220, 220, 255])
|
|
42
|
+
shape = shape_for(collision_shape)
|
|
43
|
+
position_vector = Vector3.coerce(position)
|
|
44
|
+
orientation_quaternion = Quaternion.coerce(orientation)
|
|
45
|
+
transform = Transform.new(orientation_quaternion, position_vector)
|
|
46
|
+
motion_state = MotionState.new(transform)
|
|
47
|
+
info = RigidBodyConstructionInfo.new(Float(mass), motion_state, shape)
|
|
48
|
+
body = RigidBody.new(info)
|
|
49
|
+
world.add_rigid_body(body)
|
|
50
|
+
|
|
51
|
+
@bodies << body
|
|
52
|
+
@body_specs << {
|
|
53
|
+
mass: Float(mass),
|
|
54
|
+
collision_shape: Integer(collision_shape),
|
|
55
|
+
position: position_vector.to_a,
|
|
56
|
+
orientation: orientation_quaternion.to_a,
|
|
57
|
+
rgba_color: rgba_color_argument(rgba_color)
|
|
58
|
+
}
|
|
59
|
+
@bodies.length - 1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_constraint(type, **options)
|
|
63
|
+
constraint = build_constraint(type.to_sym, options)
|
|
64
|
+
world.add_constraint(constraint, options.fetch(:disable_collisions_between_linked_bodies, false))
|
|
65
|
+
|
|
66
|
+
@constraints << constraint
|
|
67
|
+
@constraint_specs << { type: type.to_sym, options: serializable_options(options) }
|
|
68
|
+
@constraints.length - 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def load_urdf(filename, base_position: [0, 0, 0], base_orientation: Quaternion.identity, use_fixed_base: false, global_scaling: 1.0)
|
|
72
|
+
path = Data.find(filename)
|
|
73
|
+
document = REXML::Document.new(File.read(path))
|
|
74
|
+
links = REXML::XPath.match(document, "/robot/link")
|
|
75
|
+
joints = REXML::XPath.match(document, "/robot/joint")
|
|
76
|
+
raise ArgumentError, "URDF must contain at least one link: #{filename}" if links.empty?
|
|
77
|
+
return load_multilink_urdf(links, joints, base_position, base_orientation, use_fixed_base, Float(global_scaling), File.dirname(path)) if links.length > 1 || joints.any?
|
|
78
|
+
|
|
79
|
+
link = links.first
|
|
80
|
+
mass = use_fixed_base ? 0.0 : urdf_link_mass(link)
|
|
81
|
+
shape = urdf_collision_shape(link, Float(global_scaling), File.dirname(path))
|
|
82
|
+
shape_id = register_shape(shape, urdf_shape_spec(link, Float(global_scaling), File.dirname(path)))
|
|
83
|
+
body_id = create_rigid_body(
|
|
84
|
+
mass: mass,
|
|
85
|
+
collision_shape: shape_id,
|
|
86
|
+
position: base_position,
|
|
87
|
+
orientation: base_orientation
|
|
88
|
+
)
|
|
89
|
+
apply_urdf_contact(link, body_for(body_id))
|
|
90
|
+
body_id
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def load_sdf(filename, base_position: [0, 0, 0], base_orientation: Quaternion.identity, global_scaling: 1.0)
|
|
94
|
+
path = Data.find(filename)
|
|
95
|
+
document = REXML::Document.new(File.read(path))
|
|
96
|
+
model_elements = REXML::XPath.match(document, "//model")
|
|
97
|
+
raise ArgumentError, "SDF must contain at least one model: #{filename}" if model_elements.empty?
|
|
98
|
+
|
|
99
|
+
model_elements.flat_map do |model|
|
|
100
|
+
load_sdf_model(model, base_position, base_orientation, Float(global_scaling), File.dirname(path))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def load_mjcf(filename, base_position: [0, 0, 0], base_orientation: Quaternion.identity, global_scaling: 1.0)
|
|
105
|
+
path = Data.find(filename)
|
|
106
|
+
document = REXML::Document.new(File.read(path))
|
|
107
|
+
body_elements = REXML::XPath.match(document, "/mujoco/worldbody/body")
|
|
108
|
+
raise ArgumentError, "MJCF must contain at least one worldbody body: #{filename}" if body_elements.empty?
|
|
109
|
+
|
|
110
|
+
base_transform = Transform.new(Quaternion.coerce(base_orientation), Vector3.coerce(base_position))
|
|
111
|
+
body_elements.flat_map do |body|
|
|
112
|
+
load_mjcf_body(body, base_transform, Float(global_scaling), File.dirname(path))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def get_base_position_and_orientation(body_id)
|
|
117
|
+
transform = body_for(body_id).world_transform
|
|
118
|
+
[transform.origin.to_a, transform.rotation.to_a]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def set_base_position_and_orientation(body_id, position, orientation = Quaternion.identity)
|
|
122
|
+
body = body_for(body_id)
|
|
123
|
+
body.world_transform = Transform.new(Quaternion.coerce(orientation), Vector3.coerce(position))
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get_aabb(body_id)
|
|
128
|
+
body = body_for(body_id)
|
|
129
|
+
body.collision_shape.aabb(body.world_transform).map(&:to_a)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def ray_test(from, to)
|
|
133
|
+
world.ray_test(from, to)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ray_test_all(from, to)
|
|
137
|
+
world.ray_test_all(from, to)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def ray_test_batch(rays)
|
|
141
|
+
rays.map do |ray|
|
|
142
|
+
from, to = ray
|
|
143
|
+
ray_test(from, to)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def get_contact_points(body_a: nil, body_b: nil)
|
|
148
|
+
contacts = if body_a && body_b
|
|
149
|
+
world.contact_pair_test(body_for(body_a), body_for(body_b))
|
|
150
|
+
elsif body_a
|
|
151
|
+
world.contact_test(body_for(body_a))
|
|
152
|
+
elsif body_b
|
|
153
|
+
world.contact_test(body_for(body_b))
|
|
154
|
+
else
|
|
155
|
+
manifold_contacts
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
contacts.map { |contact| contact_with_body_ids(contact) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def change_dynamics(body_id, lateral_friction: nil, restitution: nil)
|
|
162
|
+
body = body_for(body_id)
|
|
163
|
+
body.friction = Float(lateral_friction) unless lateral_friction.nil?
|
|
164
|
+
body.restitution = Float(restitution) unless restitution.nil?
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def get_num_joints(body_id)
|
|
169
|
+
model_for(body_id)[:joints].length
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_joint_info(body_id, joint_index)
|
|
173
|
+
model_for(body_id)[:joints].fetch(Integer(joint_index)) { raise ArgumentError, "unknown joint index: #{joint_index}" }.dup
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def get_joint_state(body_id, joint_index)
|
|
177
|
+
joint = get_joint_info(body_id, joint_index)
|
|
178
|
+
constraint = joint[:constraint_id] && constraint_for(joint[:constraint_id])
|
|
179
|
+
position = joint_position(joint, constraint)
|
|
180
|
+
{
|
|
181
|
+
position: position,
|
|
182
|
+
velocity: joint[:target_velocity] || 0.0,
|
|
183
|
+
reaction_forces: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
|
184
|
+
applied_torque: joint[:applied_torque] || 0.0
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def set_joint_motor_control(body_id, joint_index, control_mode:, target_position: nil, target_velocity: 0.0, force: 0.0)
|
|
189
|
+
model = model_for(body_id)
|
|
190
|
+
joint = model[:joints].fetch(Integer(joint_index)) { raise ArgumentError, "unknown joint index: #{joint_index}" }
|
|
191
|
+
constraint = joint[:constraint_id] && constraint_for(joint[:constraint_id])
|
|
192
|
+
mode = control_mode.to_sym
|
|
193
|
+
|
|
194
|
+
case mode
|
|
195
|
+
when :velocity
|
|
196
|
+
set_velocity_control(joint, constraint, Float(target_velocity), Float(force))
|
|
197
|
+
when :position
|
|
198
|
+
set_position_control(joint, constraint, Float(target_position || 0.0), Float(force))
|
|
199
|
+
when :torque
|
|
200
|
+
set_torque_control(joint, Float(force))
|
|
201
|
+
else
|
|
202
|
+
raise ArgumentError, "unsupported joint control mode: #{control_mode}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def get_camera_image(width, height, camera_eye_position: [0, 0, 5], camera_target_position: [0, 0, 0], camera_up_vector: [0, 1, 0], fov: 60.0, near: 0.01, far: 100.0, background_color: [0, 0, 0, 255], body_colors: {})
|
|
209
|
+
width = Integer(width)
|
|
210
|
+
height = Integer(height)
|
|
211
|
+
raise ArgumentError, "width and height must be positive" unless width.positive? && height.positive?
|
|
212
|
+
|
|
213
|
+
camera = camera_basis(camera_eye_position, camera_target_position, camera_up_vector, Float(fov), Float(far))
|
|
214
|
+
background_color = rgba_color_argument(background_color)
|
|
215
|
+
body_colors = body_colors.to_h { |body_id, color| [Integer(body_id), rgba_color_argument(color)] }
|
|
216
|
+
rgb = []
|
|
217
|
+
depth = []
|
|
218
|
+
segmentation = []
|
|
219
|
+
|
|
220
|
+
height.times do |row|
|
|
221
|
+
width.times do |column|
|
|
222
|
+
hit = camera_ray_hit(camera, column, row, width, height)
|
|
223
|
+
append_camera_pixel(hit, rgb, depth, segmentation, background_color, body_colors)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
[width, height, rgb, depth, segmentation]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def debug_draw_world(drawer = DebugDraw.new, mode: nil)
|
|
231
|
+
drawer.debug_mode = Integer(mode) if mode
|
|
232
|
+
|
|
233
|
+
if world.respond_to?(:debug_drawer=) && world.respond_to?(:debug_draw_world)
|
|
234
|
+
world.debug_drawer = drawer
|
|
235
|
+
world.debug_draw_world
|
|
236
|
+
else
|
|
237
|
+
draw_world_fallback(drawer)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
drawer
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def save_world(filename)
|
|
244
|
+
File.write(filename, JSON.pretty_generate(world_snapshot))
|
|
245
|
+
filename
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def save_bullet(filename)
|
|
249
|
+
serializer = IO::Serializer.new(self)
|
|
250
|
+
serializer.save_bullet(self, filename)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def load_world(filename)
|
|
254
|
+
data = JSON.parse(File.read(filename), symbolize_names: true)
|
|
255
|
+
reset_simulation
|
|
256
|
+
|
|
257
|
+
data.fetch(:shapes).each { |shape| create_collision_shape(shape.fetch(:type), **symbolize_hash(shape.fetch(:options))) }
|
|
258
|
+
data.fetch(:bodies).each do |body|
|
|
259
|
+
next if body.nil?
|
|
260
|
+
|
|
261
|
+
create_rigid_body(
|
|
262
|
+
mass: body.fetch(:mass),
|
|
263
|
+
collision_shape: body.fetch(:collision_shape),
|
|
264
|
+
position: body.fetch(:position),
|
|
265
|
+
orientation: body.fetch(:orientation),
|
|
266
|
+
rgba_color: body.fetch(:rgba_color, [220, 220, 220, 255])
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
data.fetch(:constraints, []).each do |constraint|
|
|
270
|
+
next if constraint.nil?
|
|
271
|
+
|
|
272
|
+
create_constraint(constraint.fetch(:type), **symbolize_hash(constraint.fetch(:options)))
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def load_bullet(filename)
|
|
279
|
+
raise Bullet3::Error, "native extension is required for .bullet import" unless IO.const_defined?(:BulletWorldImporter, false)
|
|
280
|
+
|
|
281
|
+
importer = IO::BulletWorldImporter.new(world)
|
|
282
|
+
raise Bullet3::Error, "failed to load .bullet file: #{filename}" unless importer.load_file(filename)
|
|
283
|
+
|
|
284
|
+
@importers << importer
|
|
285
|
+
importer
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def reset_simulation
|
|
289
|
+
clear_importers
|
|
290
|
+
@constraints.compact.each { |constraint| world.remove_constraint(constraint) }
|
|
291
|
+
@bodies.compact.each { |body| world.remove_rigid_body(body) }
|
|
292
|
+
@world = DiscreteDynamicsWorld.create
|
|
293
|
+
@bodies.clear
|
|
294
|
+
@body_specs.clear
|
|
295
|
+
@shapes.clear
|
|
296
|
+
@shape_specs.clear
|
|
297
|
+
@constraints.clear
|
|
298
|
+
@constraint_specs.clear
|
|
299
|
+
@models.clear
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def body(body_id)
|
|
304
|
+
body_for(body_id)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def collision_shape(shape_id)
|
|
308
|
+
shape_for(shape_id)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def constraint(constraint_id)
|
|
312
|
+
constraint_for(constraint_id)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def remove_body(body_id)
|
|
316
|
+
body = body_for(body_id)
|
|
317
|
+
world.remove_rigid_body(body)
|
|
318
|
+
@bodies[Integer(body_id)] = nil
|
|
319
|
+
@body_specs[Integer(body_id)] = nil
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def remove_constraint(constraint_id)
|
|
324
|
+
constraint = constraint_for(constraint_id)
|
|
325
|
+
world.remove_constraint(constraint)
|
|
326
|
+
@constraints[Integer(constraint_id)] = nil
|
|
327
|
+
@constraint_specs[Integer(constraint_id)] = nil
|
|
328
|
+
nil
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def disconnect
|
|
332
|
+
clear_importers
|
|
333
|
+
@constraints.compact.each { |constraint| world.remove_constraint(constraint) }
|
|
334
|
+
@bodies.compact.each { |body| world.remove_rigid_body(body) }
|
|
335
|
+
@constraints.clear
|
|
336
|
+
@constraint_specs.clear
|
|
337
|
+
@bodies.clear
|
|
338
|
+
@body_specs.clear
|
|
339
|
+
@shapes.clear
|
|
340
|
+
@shape_specs.clear
|
|
341
|
+
@models.clear
|
|
342
|
+
nil
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private
|
|
346
|
+
|
|
347
|
+
def body_for(body_id)
|
|
348
|
+
@bodies.fetch(Integer(body_id)) { raise ArgumentError, "unknown body id: #{body_id}" } ||
|
|
349
|
+
raise(ArgumentError, "unknown body id: #{body_id}")
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def body_id_for(body)
|
|
353
|
+
return nil unless body
|
|
354
|
+
|
|
355
|
+
@bodies.index(body)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def shape_for(shape_id)
|
|
359
|
+
@shapes.fetch(Integer(shape_id)) { raise ArgumentError, "unknown collision shape id: #{shape_id}" } ||
|
|
360
|
+
raise(ArgumentError, "unknown collision shape id: #{shape_id}")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def constraint_for(constraint_id)
|
|
364
|
+
@constraints.fetch(Integer(constraint_id)) { raise ArgumentError, "unknown constraint id: #{constraint_id}" } ||
|
|
365
|
+
raise(ArgumentError, "unknown constraint id: #{constraint_id}")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def model_for(body_id)
|
|
369
|
+
@models.fetch(Integer(body_id)) { raise ArgumentError, "body has no joints: #{body_id}" }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def register_shape(shape, spec = nil)
|
|
373
|
+
@shapes << shape
|
|
374
|
+
@shape_specs << spec
|
|
375
|
+
@shapes.length - 1
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def vector_argument(x, y, z)
|
|
379
|
+
return Vector3.coerce(x) if y.nil? && z.nil?
|
|
380
|
+
|
|
381
|
+
Vector3.new(x, y, z)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def rgba_color_argument(value)
|
|
385
|
+
values = value.to_a
|
|
386
|
+
raise ArgumentError, "RGBA color must contain four components" unless values.length == 4
|
|
387
|
+
|
|
388
|
+
values.map { |component| Integer(component).clamp(0, 255) }
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def build_collision_shape(type, options)
|
|
392
|
+
case type
|
|
393
|
+
when :box
|
|
394
|
+
Shapes::BoxShape.new(Vector3.coerce(options.fetch(:half_extents)))
|
|
395
|
+
when :sphere
|
|
396
|
+
Shapes::SphereShape.new(Float(options.fetch(:radius)))
|
|
397
|
+
when :capsule
|
|
398
|
+
Shapes::CapsuleShape.new(Float(options.fetch(:radius)), Float(options.fetch(:height)))
|
|
399
|
+
when :cylinder
|
|
400
|
+
Shapes::CylinderShape.new(Vector3.coerce(options.fetch(:half_extents)))
|
|
401
|
+
when :cone
|
|
402
|
+
Shapes::ConeShape.new(Float(options.fetch(:radius)), Float(options.fetch(:height)))
|
|
403
|
+
when :plane, :static_plane
|
|
404
|
+
Shapes::StaticPlaneShape.new(Vector3.coerce(options.fetch(:normal, [0, 1, 0])), Float(options.fetch(:offset, 0.0)))
|
|
405
|
+
when :mesh, :triangle_mesh
|
|
406
|
+
triangles = options[:triangles] || load_obj_triangles(Data.find(options.fetch(:filename)), vector_scale(options[:scale], 1.0))
|
|
407
|
+
Shapes::TriangleMeshShape.new(triangles)
|
|
408
|
+
else
|
|
409
|
+
raise ArgumentError, "unsupported collision shape: #{type}"
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def build_constraint(type, options)
|
|
414
|
+
body_a = body_for(options.fetch(:body_a))
|
|
415
|
+
body_b = options.key?(:body_b) && !options[:body_b].nil? ? body_for(options[:body_b]) : nil
|
|
416
|
+
|
|
417
|
+
case type
|
|
418
|
+
when :point2point, :p2p
|
|
419
|
+
build_point2point_constraint(body_a, body_b, options)
|
|
420
|
+
when :hinge
|
|
421
|
+
build_hinge_constraint(body_a, body_b, options)
|
|
422
|
+
when :fixed
|
|
423
|
+
require_body_b!(body_b, type)
|
|
424
|
+
Constraints::FixedConstraint.new(body_a, body_b, transform_option(options, :frame_in_a), transform_option(options, :frame_in_b))
|
|
425
|
+
when :slider
|
|
426
|
+
build_frame_constraint(Constraints::SliderConstraint, body_a, body_b, options)
|
|
427
|
+
when :cone_twist
|
|
428
|
+
build_frame_constraint(Constraints::ConeTwistConstraint, body_a, body_b, options)
|
|
429
|
+
when :generic_6dof
|
|
430
|
+
build_frame_constraint(Constraints::Generic6DofConstraint, body_a, body_b, options)
|
|
431
|
+
when :generic_6dof_spring2
|
|
432
|
+
build_frame_constraint(Constraints::Generic6DofSpring2Constraint, body_a, body_b, options, options.fetch(:rotate_order, 0))
|
|
433
|
+
when :gear
|
|
434
|
+
require_body_b!(body_b, type)
|
|
435
|
+
Constraints::GearConstraint.new(
|
|
436
|
+
body_a,
|
|
437
|
+
body_b,
|
|
438
|
+
options.fetch(:axis_in_a, [0, 1, 0]),
|
|
439
|
+
options.fetch(:axis_in_b, [0, 1, 0]),
|
|
440
|
+
Float(options.fetch(:ratio, 1.0))
|
|
441
|
+
)
|
|
442
|
+
when :hinge2
|
|
443
|
+
require_body_b!(body_b, type)
|
|
444
|
+
Constraints::Hinge2Constraint.new(
|
|
445
|
+
body_a,
|
|
446
|
+
body_b,
|
|
447
|
+
options.fetch(:anchor, [0, 0, 0]),
|
|
448
|
+
options.fetch(:axis_in_a, [0, 1, 0]),
|
|
449
|
+
options.fetch(:axis_in_b, [1, 0, 0])
|
|
450
|
+
)
|
|
451
|
+
else
|
|
452
|
+
raise ArgumentError, "unsupported constraint type: #{type}"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def build_point2point_constraint(body_a, body_b, options)
|
|
457
|
+
pivot_a = options.fetch(:pivot_in_a, [0, 0, 0])
|
|
458
|
+
return Constraints::Point2PointConstraint.new(body_a, pivot_a) unless body_b
|
|
459
|
+
|
|
460
|
+
Constraints::Point2PointConstraint.new(body_a, body_b, pivot_a, options.fetch(:pivot_in_b, [0, 0, 0]))
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def build_hinge_constraint(body_a, body_b, options)
|
|
464
|
+
pivot_a = options.fetch(:pivot_in_a, [0, 0, 0])
|
|
465
|
+
axis_a = options.fetch(:axis_in_a, [0, 1, 0])
|
|
466
|
+
use_reference_frame_a = options.fetch(:use_reference_frame_a, false)
|
|
467
|
+
return Constraints::HingeConstraint.new(body_a, pivot_a, axis_a, use_reference_frame_a) unless body_b
|
|
468
|
+
|
|
469
|
+
Constraints::HingeConstraint.new(
|
|
470
|
+
body_a,
|
|
471
|
+
body_b,
|
|
472
|
+
pivot_a,
|
|
473
|
+
options.fetch(:pivot_in_b, [0, 0, 0]),
|
|
474
|
+
axis_a,
|
|
475
|
+
options.fetch(:axis_in_b, [0, 1, 0]),
|
|
476
|
+
use_reference_frame_a
|
|
477
|
+
)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def build_frame_constraint(klass, body_a, body_b, options, *extra_args)
|
|
481
|
+
frame_a = transform_option(options, :frame_in_a)
|
|
482
|
+
return klass.new(body_a, frame_a, *extra_args) unless body_b
|
|
483
|
+
|
|
484
|
+
klass.new(body_a, body_b, frame_a, transform_option(options, :frame_in_b), *extra_args)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def transform_option(options, key)
|
|
488
|
+
value = options.fetch(key, Transform.identity)
|
|
489
|
+
return value if value.is_a?(Transform)
|
|
490
|
+
|
|
491
|
+
Transform.new(Quaternion.coerce(value.fetch(:orientation, Quaternion.identity)), Vector3.coerce(value.fetch(:position, [0, 0, 0])))
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def require_body_b!(body_b, type)
|
|
495
|
+
raise ArgumentError, "#{type} constraint requires body_b" unless body_b
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def load_multilink_urdf(links, joints, base_position, base_orientation, use_fixed_base, global_scaling, base_path)
|
|
499
|
+
links_by_name = links.to_h { |link| [link.attributes["name"], link] }
|
|
500
|
+
root_name = urdf_root_link_name(links_by_name, joints)
|
|
501
|
+
base_transform = Transform.new(Quaternion.coerce(base_orientation), Vector3.coerce(base_position))
|
|
502
|
+
link_transforms = { root_name => base_transform }
|
|
503
|
+
link_body_ids = {
|
|
504
|
+
root_name => create_urdf_link_body(links_by_name.fetch(root_name), base_transform, use_fixed_base, true, global_scaling, base_path)
|
|
505
|
+
}
|
|
506
|
+
joint_infos = []
|
|
507
|
+
pending_joints = joints.dup
|
|
508
|
+
|
|
509
|
+
until pending_joints.empty?
|
|
510
|
+
progressed = false
|
|
511
|
+
pending_joints.delete_if do |joint|
|
|
512
|
+
parent_name = urdf_joint_parent(joint)
|
|
513
|
+
child_name = urdf_joint_child(joint)
|
|
514
|
+
next false unless link_body_ids[parent_name]
|
|
515
|
+
|
|
516
|
+
origin = urdf_origin_transform(REXML::XPath.first(joint, "origin"), global_scaling)
|
|
517
|
+
child_transform = link_transforms.fetch(parent_name) * origin
|
|
518
|
+
link_transforms[child_name] = child_transform
|
|
519
|
+
link_body_ids[child_name] = create_urdf_link_body(links_by_name.fetch(child_name), child_transform, false, false, global_scaling, base_path)
|
|
520
|
+
joint_infos << create_urdf_joint(joint, link_body_ids.fetch(parent_name), link_body_ids.fetch(child_name), origin)
|
|
521
|
+
progressed = true
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
raise ArgumentError, "URDF joint graph is disconnected or cyclic" unless progressed
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
base_id = link_body_ids.fetch(root_name)
|
|
528
|
+
@models[base_id] = {
|
|
529
|
+
links: link_body_ids.keys,
|
|
530
|
+
body_ids: link_body_ids.values,
|
|
531
|
+
joints: joint_infos
|
|
532
|
+
}
|
|
533
|
+
base_id
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def urdf_root_link_name(links_by_name, joints)
|
|
537
|
+
child_names = joints.map { |joint| urdf_joint_child(joint) }
|
|
538
|
+
(links_by_name.keys - child_names).first || links_by_name.keys.first
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def urdf_joint_parent(joint)
|
|
542
|
+
REXML::XPath.first(joint, "parent").attributes["link"]
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def urdf_joint_child(joint)
|
|
546
|
+
REXML::XPath.first(joint, "child").attributes["link"]
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def create_urdf_link_body(link, transform, use_fixed_base, root_link, global_scaling, base_path)
|
|
550
|
+
shape = urdf_link_collision_shape(link, global_scaling, base_path)
|
|
551
|
+
shape_id = register_shape(shape, urdf_shape_spec(link, global_scaling, base_path))
|
|
552
|
+
body_id = create_rigid_body(
|
|
553
|
+
mass: use_fixed_base && root_link ? 0.0 : urdf_link_mass(link),
|
|
554
|
+
collision_shape: shape_id,
|
|
555
|
+
position: transform.origin.to_a,
|
|
556
|
+
orientation: transform.rotation.to_a
|
|
557
|
+
)
|
|
558
|
+
apply_urdf_contact(link, body_for(body_id))
|
|
559
|
+
body_id
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def urdf_link_collision_shape(link, global_scaling, base_path)
|
|
563
|
+
urdf_collision_shape(link, global_scaling, base_path)
|
|
564
|
+
rescue ArgumentError => error
|
|
565
|
+
raise unless error.message.include?("no collision geometry")
|
|
566
|
+
|
|
567
|
+
Shapes::SphereShape.new(0.001 * global_scaling)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def create_urdf_joint(joint, parent_body_id, child_body_id, origin)
|
|
571
|
+
type = joint.attributes["type"].to_s
|
|
572
|
+
axis = urdf_joint_axis(joint)
|
|
573
|
+
limits = urdf_joint_limits(joint)
|
|
574
|
+
constraint_id = case type
|
|
575
|
+
when "fixed"
|
|
576
|
+
create_constraint(:fixed, body_a: parent_body_id, body_b: child_body_id, frame_in_a: origin, frame_in_b: Transform.identity)
|
|
577
|
+
when "revolute", "continuous"
|
|
578
|
+
id = create_constraint(
|
|
579
|
+
:hinge,
|
|
580
|
+
body_a: parent_body_id,
|
|
581
|
+
body_b: child_body_id,
|
|
582
|
+
pivot_in_a: origin.origin.to_a,
|
|
583
|
+
pivot_in_b: [0, 0, 0],
|
|
584
|
+
axis_in_a: axis,
|
|
585
|
+
axis_in_b: axis
|
|
586
|
+
)
|
|
587
|
+
constraint(id).set_limit(limits[:lower], limits[:upper]) if type == "revolute" && limits[:lower] && limits[:upper]
|
|
588
|
+
id
|
|
589
|
+
when "prismatic"
|
|
590
|
+
id = create_constraint(:slider, body_a: parent_body_id, body_b: child_body_id, frame_in_a: origin, frame_in_b: Transform.identity)
|
|
591
|
+
constraint(id).lower_linear_limit = limits[:lower] if limits[:lower]
|
|
592
|
+
constraint(id).upper_linear_limit = limits[:upper] if limits[:upper]
|
|
593
|
+
id
|
|
594
|
+
else
|
|
595
|
+
create_constraint(:fixed, body_a: parent_body_id, body_b: child_body_id, frame_in_a: origin, frame_in_b: Transform.identity)
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
{
|
|
599
|
+
name: joint.attributes["name"].to_s,
|
|
600
|
+
type: type.empty? ? "fixed" : type,
|
|
601
|
+
parent_body: parent_body_id,
|
|
602
|
+
child_body: child_body_id,
|
|
603
|
+
constraint_id: constraint_id,
|
|
604
|
+
axis: axis,
|
|
605
|
+
lower_limit: limits[:lower],
|
|
606
|
+
upper_limit: limits[:upper],
|
|
607
|
+
target_velocity: 0.0,
|
|
608
|
+
applied_torque: 0.0
|
|
609
|
+
}
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def urdf_joint_axis(joint)
|
|
613
|
+
axis = REXML::XPath.first(joint, "axis")
|
|
614
|
+
axis ? float_list(axis.attributes["xyz"]) : [1.0, 0.0, 0.0]
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def urdf_joint_limits(joint)
|
|
618
|
+
limit = REXML::XPath.first(joint, "limit")
|
|
619
|
+
return {} unless limit
|
|
620
|
+
|
|
621
|
+
{
|
|
622
|
+
lower: limit.attributes["lower"] && Float(limit.attributes["lower"]),
|
|
623
|
+
upper: limit.attributes["upper"] && Float(limit.attributes["upper"]),
|
|
624
|
+
effort: limit.attributes["effort"] && Float(limit.attributes["effort"]),
|
|
625
|
+
velocity: limit.attributes["velocity"] && Float(limit.attributes["velocity"])
|
|
626
|
+
}
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def joint_position(joint, constraint)
|
|
630
|
+
return 0.0 unless constraint
|
|
631
|
+
|
|
632
|
+
case joint[:type]
|
|
633
|
+
when "revolute", "continuous"
|
|
634
|
+
constraint.angle
|
|
635
|
+
when "prismatic"
|
|
636
|
+
constraint.linear_position
|
|
637
|
+
else
|
|
638
|
+
0.0
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def set_velocity_control(joint, constraint, target_velocity, force)
|
|
643
|
+
joint[:target_velocity] = target_velocity
|
|
644
|
+
joint[:applied_torque] = force
|
|
645
|
+
case joint[:type]
|
|
646
|
+
when "revolute", "continuous"
|
|
647
|
+
constraint&.enable_angular_motor(true, target_velocity, force)
|
|
648
|
+
when "prismatic"
|
|
649
|
+
constraint.powered_linear_motor = true if constraint
|
|
650
|
+
constraint.target_linear_motor_velocity = target_velocity if constraint
|
|
651
|
+
constraint.max_linear_motor_force = force if constraint
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def set_position_control(joint, constraint, target_position, force)
|
|
656
|
+
joint[:target_velocity] = 0.0
|
|
657
|
+
joint[:applied_torque] = force
|
|
658
|
+
case joint[:type]
|
|
659
|
+
when "revolute", "continuous"
|
|
660
|
+
constraint&.set_limit(target_position, target_position)
|
|
661
|
+
constraint&.enable_angular_motor(true, 0.0, force)
|
|
662
|
+
when "prismatic"
|
|
663
|
+
if constraint
|
|
664
|
+
constraint.lower_linear_limit = target_position
|
|
665
|
+
constraint.upper_linear_limit = target_position
|
|
666
|
+
constraint.max_linear_motor_force = force
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def set_torque_control(joint, force)
|
|
672
|
+
joint[:applied_torque] = force
|
|
673
|
+
axis = joint[:axis]
|
|
674
|
+
body_for(joint.fetch(:child_body)).apply_torque(axis.map { |component| component * force })
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def load_sdf_model(model, base_position, base_orientation, global_scaling, base_path)
|
|
678
|
+
model_transform = Transform.new(Quaternion.coerce(base_orientation), Vector3.coerce(base_position)) *
|
|
679
|
+
sdf_pose_transform(REXML::XPath.first(model, "pose"), global_scaling)
|
|
680
|
+
|
|
681
|
+
REXML::XPath.match(model, "link").map do |link|
|
|
682
|
+
link_transform = model_transform * sdf_pose_transform(REXML::XPath.first(link, "pose"), global_scaling)
|
|
683
|
+
shape = sdf_link_collision_shape(link, global_scaling, base_path)
|
|
684
|
+
shape_id = register_shape(shape)
|
|
685
|
+
create_rigid_body(
|
|
686
|
+
mass: sdf_link_mass(link),
|
|
687
|
+
collision_shape: shape_id,
|
|
688
|
+
position: link_transform.origin.to_a,
|
|
689
|
+
orientation: link_transform.rotation.to_a
|
|
690
|
+
)
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def sdf_link_mass(link)
|
|
695
|
+
mass = REXML::XPath.first(link, "inertial/mass")
|
|
696
|
+
mass ? Float(mass.text) : 0.0
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def sdf_link_collision_shape(link, global_scaling, base_path)
|
|
700
|
+
collisions = REXML::XPath.match(link, "collision")
|
|
701
|
+
raise ArgumentError, "SDF link has no collision geometry" if collisions.empty?
|
|
702
|
+
|
|
703
|
+
shapes = collisions.map do |collision|
|
|
704
|
+
geometry = REXML::XPath.first(collision, "geometry")
|
|
705
|
+
[sdf_geometry_shape(geometry, global_scaling, base_path), sdf_pose_transform(REXML::XPath.first(collision, "pose"), global_scaling)]
|
|
706
|
+
end
|
|
707
|
+
return shapes.first.first if shapes.length == 1 && identity_transform?(shapes.first.last)
|
|
708
|
+
|
|
709
|
+
compound = Shapes::CompoundShape.new
|
|
710
|
+
shapes.each { |shape, transform| compound.add_child_shape(transform, shape) }
|
|
711
|
+
compound
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def sdf_geometry_shape(geometry, global_scaling, base_path)
|
|
715
|
+
if (box = REXML::XPath.first(geometry, "box"))
|
|
716
|
+
half_extents = float_list(REXML::XPath.first(box, "size").text).map { |value| value * global_scaling * 0.5 }
|
|
717
|
+
return Shapes::BoxShape.new(Vector3.coerce(half_extents))
|
|
718
|
+
end
|
|
719
|
+
if (sphere = REXML::XPath.first(geometry, "sphere"))
|
|
720
|
+
return Shapes::SphereShape.new(Float(REXML::XPath.first(sphere, "radius").text) * global_scaling)
|
|
721
|
+
end
|
|
722
|
+
if (cylinder = REXML::XPath.first(geometry, "cylinder"))
|
|
723
|
+
radius = Float(REXML::XPath.first(cylinder, "radius").text) * global_scaling
|
|
724
|
+
length = Float(REXML::XPath.first(cylinder, "length").text) * global_scaling
|
|
725
|
+
return Shapes::CylinderShape.new(Vector3.new(radius, length * 0.5, radius))
|
|
726
|
+
end
|
|
727
|
+
if (plane = REXML::XPath.first(geometry, "plane"))
|
|
728
|
+
normal = REXML::XPath.first(plane, "normal")&.text || "0 0 1"
|
|
729
|
+
return Shapes::StaticPlaneShape.new(Vector3.coerce(float_list(normal)), 0.0)
|
|
730
|
+
end
|
|
731
|
+
if (mesh = REXML::XPath.first(geometry, "mesh"))
|
|
732
|
+
uri = REXML::XPath.first(mesh, "uri").text
|
|
733
|
+
scale = vector_scale(REXML::XPath.first(mesh, "scale")&.text, global_scaling)
|
|
734
|
+
return Shapes::TriangleMeshShape.new(load_obj_triangles(resolve_mesh_filename(uri, base_path), scale))
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
raise ArgumentError, "unsupported SDF collision geometry"
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def sdf_pose_transform(pose, global_scaling)
|
|
741
|
+
return Transform.identity unless pose
|
|
742
|
+
|
|
743
|
+
values = float_list(pose.text)
|
|
744
|
+
xyz = [values[0] || 0.0, values[1] || 0.0, values[2] || 0.0].map { |value| value * global_scaling }
|
|
745
|
+
rpy = [values[3] || 0.0, values[4] || 0.0, values[5] || 0.0]
|
|
746
|
+
Transform.new(Quaternion.from_euler(*rpy), Vector3.coerce(xyz))
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def load_mjcf_body(body_element, parent_transform, global_scaling, base_path)
|
|
750
|
+
body_transform = parent_transform * mjcf_body_transform(body_element, global_scaling)
|
|
751
|
+
body_ids = []
|
|
752
|
+
geoms = REXML::XPath.match(body_element, "geom")
|
|
753
|
+
unless geoms.empty?
|
|
754
|
+
shape = mjcf_geoms_shape(geoms, global_scaling, base_path)
|
|
755
|
+
shape_id = register_shape(shape)
|
|
756
|
+
body_ids << create_rigid_body(
|
|
757
|
+
mass: mjcf_body_mass(body_element, geoms),
|
|
758
|
+
collision_shape: shape_id,
|
|
759
|
+
position: body_transform.origin.to_a,
|
|
760
|
+
orientation: body_transform.rotation.to_a
|
|
761
|
+
)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
REXML::XPath.match(body_element, "body").each do |child|
|
|
765
|
+
body_ids.concat(load_mjcf_body(child, body_transform, global_scaling, base_path))
|
|
766
|
+
end
|
|
767
|
+
body_ids
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def mjcf_body_transform(body_element, global_scaling)
|
|
771
|
+
position = float_list(body_element.attributes["pos"] || "0 0 0").map { |value| value * global_scaling }
|
|
772
|
+
orientation = body_element.attributes["quat"] ? Quaternion.coerce(float_list(body_element.attributes["quat"])) : Quaternion.identity
|
|
773
|
+
Transform.new(orientation, Vector3.coerce(position))
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def mjcf_body_mass(body_element, geoms)
|
|
777
|
+
Float(body_element.attributes["mass"] || geoms.lazy.map { |geom| geom.attributes["mass"] }.find(&:itself) || 1.0)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def mjcf_geoms_shape(geoms, global_scaling, base_path)
|
|
781
|
+
shapes = geoms.map do |geom|
|
|
782
|
+
[mjcf_geom_shape(geom, global_scaling, base_path), mjcf_geom_transform(geom, global_scaling)]
|
|
783
|
+
end
|
|
784
|
+
return shapes.first.first if shapes.length == 1 && identity_transform?(shapes.first.last)
|
|
785
|
+
|
|
786
|
+
compound = Shapes::CompoundShape.new
|
|
787
|
+
shapes.each { |shape, transform| compound.add_child_shape(transform, shape) }
|
|
788
|
+
compound
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def mjcf_geom_shape(geom, global_scaling, base_path)
|
|
792
|
+
type = geom.attributes["type"] || "sphere"
|
|
793
|
+
size = float_list(geom.attributes["size"] || "1")
|
|
794
|
+
case type
|
|
795
|
+
when "box"
|
|
796
|
+
Shapes::BoxShape.new(Vector3.coerce(size.first(3).map { |value| value * global_scaling }))
|
|
797
|
+
when "sphere"
|
|
798
|
+
Shapes::SphereShape.new(Float(size.fetch(0)) * global_scaling)
|
|
799
|
+
when "capsule"
|
|
800
|
+
Shapes::CapsuleShape.new(Float(size.fetch(0)) * global_scaling, Float(size.fetch(1, size.fetch(0))) * global_scaling * 2.0)
|
|
801
|
+
when "cylinder"
|
|
802
|
+
radius = Float(size.fetch(0)) * global_scaling
|
|
803
|
+
half_height = Float(size.fetch(1, size.fetch(0))) * global_scaling
|
|
804
|
+
Shapes::CylinderShape.new(Vector3.new(radius, half_height, radius))
|
|
805
|
+
when "mesh"
|
|
806
|
+
filename = geom.attributes["file"] || geom.attributes["mesh"]
|
|
807
|
+
Shapes::TriangleMeshShape.new(load_obj_triangles(resolve_mesh_filename(filename, base_path), [global_scaling, global_scaling, global_scaling]))
|
|
808
|
+
else
|
|
809
|
+
raise ArgumentError, "unsupported MJCF geom type: #{type}"
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def mjcf_geom_transform(geom, global_scaling)
|
|
814
|
+
position = float_list(geom.attributes["pos"] || "0 0 0").map { |value| value * global_scaling }
|
|
815
|
+
orientation = geom.attributes["quat"] ? Quaternion.coerce(float_list(geom.attributes["quat"])) : Quaternion.identity
|
|
816
|
+
Transform.new(orientation, Vector3.coerce(position))
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def camera_basis(eye, target, up, fov, far)
|
|
820
|
+
eye = vector_array(eye)
|
|
821
|
+
target = vector_array(target)
|
|
822
|
+
forward = normalize_vector(vector_subtract(target, eye))
|
|
823
|
+
right = normalize_vector(cross_vector(forward, vector_array(up)))
|
|
824
|
+
camera_up = normalize_vector(cross_vector(right, forward))
|
|
825
|
+
{
|
|
826
|
+
eye: eye,
|
|
827
|
+
forward: forward,
|
|
828
|
+
right: right,
|
|
829
|
+
up: camera_up,
|
|
830
|
+
scale: Math.tan(fov * Math::PI / 360.0),
|
|
831
|
+
far: far
|
|
832
|
+
}
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def camera_ray_hit(camera, column, row, width, height)
|
|
836
|
+
aspect = width.to_f / height
|
|
837
|
+
x = ((column + 0.5) / width * 2.0 - 1.0) * aspect * camera.fetch(:scale)
|
|
838
|
+
y = (1.0 - (row + 0.5) / height * 2.0) * camera.fetch(:scale)
|
|
839
|
+
direction = normalize_vector(vector_add(camera.fetch(:forward), vector_add(vector_scale_array(camera.fetch(:right), x), vector_scale_array(camera.fetch(:up), y))))
|
|
840
|
+
ray_to = vector_add(camera.fetch(:eye), vector_scale_array(direction, camera.fetch(:far)))
|
|
841
|
+
ray_test(camera.fetch(:eye), ray_to)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def append_camera_pixel(hit, rgb, depth, segmentation, background_color, body_colors)
|
|
845
|
+
if hit
|
|
846
|
+
body_id = body_id_for(hit[:body]) || -1
|
|
847
|
+
rgb.concat(body_colors.fetch(body_id) { body_color(body_id) })
|
|
848
|
+
depth << hit[:fraction]
|
|
849
|
+
segmentation << body_id
|
|
850
|
+
else
|
|
851
|
+
rgb.concat(background_color)
|
|
852
|
+
depth << 1.0
|
|
853
|
+
segmentation << -1
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def body_color(body_id)
|
|
858
|
+
return [220, 220, 220, 255] if body_id.negative?
|
|
859
|
+
|
|
860
|
+
@body_specs.fetch(body_id, nil)&.fetch(:rgba_color, nil) || [220, 220, 220, 255]
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def draw_world_fallback(drawer)
|
|
864
|
+
debug_mode = drawer.respond_to?(:debug_mode) ? drawer.debug_mode : DebugDraw::DRAW_AABB
|
|
865
|
+
draw_aabbs(drawer) unless (debug_mode & DebugDraw::DRAW_AABB).zero?
|
|
866
|
+
draw_contacts(drawer) unless (debug_mode & DebugDraw::DRAW_CONTACT_POINTS).zero?
|
|
867
|
+
drawer
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def draw_aabbs(drawer)
|
|
871
|
+
@bodies.each_index do |body_id|
|
|
872
|
+
next unless @bodies[body_id]
|
|
873
|
+
|
|
874
|
+
draw_aabb(drawer, get_aabb(body_id), [1.0, 0.0, 0.0])
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def draw_aabb(drawer, aabb, color)
|
|
879
|
+
min, max = aabb
|
|
880
|
+
corners = [
|
|
881
|
+
[min[0], min[1], min[2]], [max[0], min[1], min[2]],
|
|
882
|
+
[max[0], max[1], min[2]], [min[0], max[1], min[2]],
|
|
883
|
+
[min[0], min[1], max[2]], [max[0], min[1], max[2]],
|
|
884
|
+
[max[0], max[1], max[2]], [min[0], max[1], max[2]]
|
|
885
|
+
]
|
|
886
|
+
[[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]].each do |from, to|
|
|
887
|
+
drawer.draw_line(corners[from], corners[to], color)
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def draw_contacts(drawer)
|
|
892
|
+
get_contact_points.each do |contact|
|
|
893
|
+
drawer.draw_contact_point(
|
|
894
|
+
contact.fetch(:position_world_on_b),
|
|
895
|
+
contact.fetch(:normal_world_on_b),
|
|
896
|
+
distance: contact.fetch(:distance),
|
|
897
|
+
life_time: 0,
|
|
898
|
+
color: [1.0, 1.0, 0.0]
|
|
899
|
+
)
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def clear_importers
|
|
904
|
+
@importers.each(&:delete_all_data)
|
|
905
|
+
@importers.clear
|
|
906
|
+
nil
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
def world_snapshot
|
|
910
|
+
{
|
|
911
|
+
shapes: @shape_specs.map { |spec| spec || raise(ArgumentError, "world contains an unserializable shape") },
|
|
912
|
+
bodies: @body_specs.each_with_index.map { |spec, index| spec && body_snapshot(index, spec) },
|
|
913
|
+
constraints: @constraint_specs
|
|
914
|
+
}
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def body_snapshot(index, spec)
|
|
918
|
+
transform = body_for(index).world_transform
|
|
919
|
+
spec.merge(position: transform.origin.to_a, orientation: transform.rotation.to_a)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def serializable_options(value)
|
|
923
|
+
case value
|
|
924
|
+
when Hash
|
|
925
|
+
value.to_h { |key, child| [key, serializable_options(child)] }
|
|
926
|
+
when Array
|
|
927
|
+
value.map { |child| serializable_options(child) }
|
|
928
|
+
when Transform
|
|
929
|
+
{ position: value.origin.to_a, orientation: value.rotation.to_a }
|
|
930
|
+
when Vector3, Quaternion
|
|
931
|
+
value.to_a
|
|
932
|
+
when Symbol
|
|
933
|
+
value.to_s
|
|
934
|
+
else
|
|
935
|
+
value
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def symbolize_hash(value)
|
|
940
|
+
case value
|
|
941
|
+
when Hash
|
|
942
|
+
value.to_h { |key, child| [key.to_sym, symbolize_hash(child)] }
|
|
943
|
+
when Array
|
|
944
|
+
value.map { |child| symbolize_hash(child) }
|
|
945
|
+
else
|
|
946
|
+
value
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def vector_array(value)
|
|
951
|
+
Vector3.coerce(value).to_a
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def vector_add(left, right)
|
|
955
|
+
left.zip(right).map { |a, b| a + b }
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def vector_subtract(left, right)
|
|
959
|
+
left.zip(right).map { |a, b| a - b }
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def vector_scale_array(vector, scalar)
|
|
963
|
+
vector.map { |component| component * scalar }
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
def cross_vector(left, right)
|
|
967
|
+
[
|
|
968
|
+
left[1] * right[2] - left[2] * right[1],
|
|
969
|
+
left[2] * right[0] - left[0] * right[2],
|
|
970
|
+
left[0] * right[1] - left[1] * right[0]
|
|
971
|
+
]
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def normalize_vector(vector)
|
|
975
|
+
length = Math.sqrt(vector.sum { |component| component * component })
|
|
976
|
+
raise ArgumentError, "cannot normalize zero-length vector" if length.zero?
|
|
977
|
+
|
|
978
|
+
vector.map { |component| component / length }
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def manifold_contacts
|
|
982
|
+
world.contact_manifolds.flat_map do |manifold|
|
|
983
|
+
manifold[:points].map do |point|
|
|
984
|
+
point.merge(body0: manifold[:body0], body1: manifold[:body1])
|
|
985
|
+
end
|
|
986
|
+
end
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def contact_with_body_ids(contact)
|
|
990
|
+
contact.merge(
|
|
991
|
+
body0: body_id_for(contact[:body0]),
|
|
992
|
+
body1: body_id_for(contact[:body1])
|
|
993
|
+
)
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def urdf_link_mass(link)
|
|
997
|
+
mass_element = REXML::XPath.first(link, "inertial/mass")
|
|
998
|
+
return 0.0 unless mass_element
|
|
999
|
+
|
|
1000
|
+
Float(mass_element.attributes["value"])
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
def urdf_shape_spec(link, global_scaling, base_path)
|
|
1004
|
+
collisions = REXML::XPath.match(link, "collision")
|
|
1005
|
+
return nil unless collisions.length == 1
|
|
1006
|
+
|
|
1007
|
+
collision = collisions.first
|
|
1008
|
+
return nil unless identity_transform?(urdf_origin_transform(REXML::XPath.first(collision, "origin"), global_scaling))
|
|
1009
|
+
|
|
1010
|
+
urdf_geometry_shape_spec(REXML::XPath.first(collision, "geometry"), global_scaling, base_path)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def urdf_geometry_shape_spec(geometry, global_scaling, base_path)
|
|
1014
|
+
if (box = REXML::XPath.first(geometry, "box"))
|
|
1015
|
+
return { type: :box, options: { half_extents: float_list(box.attributes["size"]).map { |value| value * global_scaling * 0.5 } } }
|
|
1016
|
+
end
|
|
1017
|
+
if (sphere = REXML::XPath.first(geometry, "sphere"))
|
|
1018
|
+
return { type: :sphere, options: { radius: Float(sphere.attributes["radius"]) * global_scaling } }
|
|
1019
|
+
end
|
|
1020
|
+
if (cylinder = REXML::XPath.first(geometry, "cylinder"))
|
|
1021
|
+
radius = Float(cylinder.attributes["radius"]) * global_scaling
|
|
1022
|
+
length = Float(cylinder.attributes["length"]) * global_scaling
|
|
1023
|
+
return { type: :cylinder, options: { half_extents: [radius, length * 0.5, radius] } }
|
|
1024
|
+
end
|
|
1025
|
+
if (capsule = REXML::XPath.first(geometry, "capsule"))
|
|
1026
|
+
return { type: :capsule, options: { radius: Float(capsule.attributes["radius"]) * global_scaling, height: Float(capsule.attributes["length"]) * global_scaling } }
|
|
1027
|
+
end
|
|
1028
|
+
if (mesh = REXML::XPath.first(geometry, "mesh"))
|
|
1029
|
+
return { type: :mesh, options: { filename: resolve_mesh_filename(mesh.attributes["filename"], base_path), scale: vector_scale(mesh.attributes["scale"], global_scaling) } }
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
nil
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def urdf_collision_shape(link, global_scaling, base_path)
|
|
1036
|
+
collisions = REXML::XPath.match(link, "collision")
|
|
1037
|
+
raise ArgumentError, "URDF link has no collision geometry" if collisions.empty?
|
|
1038
|
+
|
|
1039
|
+
shapes_with_origins = collisions.map do |collision|
|
|
1040
|
+
geometry = REXML::XPath.first(collision, "geometry")
|
|
1041
|
+
raise ArgumentError, "URDF collision has no geometry" unless geometry
|
|
1042
|
+
|
|
1043
|
+
[urdf_geometry_shape(geometry, global_scaling, base_path), urdf_origin_transform(REXML::XPath.first(collision, "origin"), global_scaling)]
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
return shapes_with_origins.first.first if shapes_with_origins.length == 1 && identity_transform?(shapes_with_origins.first.last)
|
|
1047
|
+
|
|
1048
|
+
compound = Shapes::CompoundShape.new
|
|
1049
|
+
shapes_with_origins.each do |shape, transform|
|
|
1050
|
+
compound.add_child_shape(transform, shape)
|
|
1051
|
+
end
|
|
1052
|
+
compound
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def urdf_geometry_shape(geometry, global_scaling, base_path)
|
|
1056
|
+
if (box = REXML::XPath.first(geometry, "box"))
|
|
1057
|
+
size = float_list(box.attributes["size"]).map { |value| value * global_scaling * 0.5 }
|
|
1058
|
+
return Shapes::BoxShape.new(Vector3.coerce(size))
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
if (sphere = REXML::XPath.first(geometry, "sphere"))
|
|
1062
|
+
return Shapes::SphereShape.new(Float(sphere.attributes["radius"]) * global_scaling)
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
if (cylinder = REXML::XPath.first(geometry, "cylinder"))
|
|
1066
|
+
radius = Float(cylinder.attributes["radius"]) * global_scaling
|
|
1067
|
+
length = Float(cylinder.attributes["length"]) * global_scaling
|
|
1068
|
+
return Shapes::CylinderShape.new(Vector3.new(radius, length * 0.5, radius))
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
if (capsule = REXML::XPath.first(geometry, "capsule"))
|
|
1072
|
+
return Shapes::CapsuleShape.new(
|
|
1073
|
+
Float(capsule.attributes["radius"]) * global_scaling,
|
|
1074
|
+
Float(capsule.attributes["length"]) * global_scaling
|
|
1075
|
+
)
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
mesh = REXML::XPath.first(geometry, "mesh")
|
|
1079
|
+
if mesh
|
|
1080
|
+
scale = vector_scale(mesh.attributes["scale"], global_scaling)
|
|
1081
|
+
return Shapes::TriangleMeshShape.new(load_obj_triangles(resolve_mesh_filename(mesh.attributes["filename"], base_path), scale))
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
raise ArgumentError, "unsupported URDF collision geometry"
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
def resolve_mesh_filename(filename, base_path)
|
|
1088
|
+
normalized = filename.to_s.sub(%r{\Afile://}, "").sub(%r{\Apackage://}, "")
|
|
1089
|
+
candidate = File.expand_path(normalized, base_path)
|
|
1090
|
+
return candidate if File.exist?(candidate)
|
|
1091
|
+
|
|
1092
|
+
Data.find(normalized)
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
def vector_scale(value, global_scaling)
|
|
1096
|
+
parts = value ? float_list(value) : [1.0, 1.0, 1.0]
|
|
1097
|
+
raise ArgumentError, "URDF mesh scale must have 3 components" unless parts.length == 3
|
|
1098
|
+
|
|
1099
|
+
parts.map { |component| component * global_scaling }
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def load_obj_triangles(path, scale)
|
|
1103
|
+
vertices = []
|
|
1104
|
+
triangles = []
|
|
1105
|
+
|
|
1106
|
+
File.foreach(path) do |line|
|
|
1107
|
+
parts = line.strip.split
|
|
1108
|
+
next if parts.empty? || parts.first.start_with?("#")
|
|
1109
|
+
|
|
1110
|
+
case parts.first
|
|
1111
|
+
when "v"
|
|
1112
|
+
vertices << scaled_vertex(parts, scale)
|
|
1113
|
+
when "f"
|
|
1114
|
+
indices = face_indices(parts.drop(1), vertices.length)
|
|
1115
|
+
(1...(indices.length - 1)).each do |index|
|
|
1116
|
+
triangles << [vertices[indices.first], vertices[indices[index]], vertices[indices[index + 1]]]
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
raise ArgumentError, "OBJ mesh has no triangles: #{path}" if triangles.empty?
|
|
1122
|
+
|
|
1123
|
+
triangles
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def scaled_vertex(parts, scale)
|
|
1127
|
+
raise ArgumentError, "OBJ vertex must have 3 components" if parts.length < 4
|
|
1128
|
+
|
|
1129
|
+
[
|
|
1130
|
+
Float(parts[1]) * scale[0],
|
|
1131
|
+
Float(parts[2]) * scale[1],
|
|
1132
|
+
Float(parts[3]) * scale[2]
|
|
1133
|
+
]
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
def face_indices(tokens, vertex_count)
|
|
1137
|
+
indices = tokens.map do |token|
|
|
1138
|
+
index = Integer(token.split("/").first)
|
|
1139
|
+
index.negative? ? vertex_count + index : index - 1
|
|
1140
|
+
end
|
|
1141
|
+
raise ArgumentError, "OBJ face must have at least 3 vertices" if indices.length < 3
|
|
1142
|
+
|
|
1143
|
+
indices
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def urdf_origin_transform(origin, global_scaling)
|
|
1147
|
+
return Transform.identity unless origin
|
|
1148
|
+
|
|
1149
|
+
xyz = float_list(origin.attributes["xyz"] || "0 0 0").map { |value| value * global_scaling }
|
|
1150
|
+
rpy = float_list(origin.attributes["rpy"] || "0 0 0")
|
|
1151
|
+
Transform.new(Quaternion.from_euler(*rpy), Vector3.coerce(xyz))
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
def identity_transform?(transform)
|
|
1155
|
+
transform.origin == Vector3.zero && transform.rotation == Quaternion.identity
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
def apply_urdf_contact(link, body)
|
|
1159
|
+
lateral_friction = REXML::XPath.first(link, "contact/lateral_friction")
|
|
1160
|
+
body.friction = Float(lateral_friction.attributes["value"]) if lateral_friction
|
|
1161
|
+
|
|
1162
|
+
restitution = REXML::XPath.first(link, "contact/restitution")
|
|
1163
|
+
body.restitution = Float(restitution.attributes["value"]) if restitution
|
|
1164
|
+
end
|
|
1165
|
+
|
|
1166
|
+
def float_list(value)
|
|
1167
|
+
value.to_s.split.map { |part| Float(part) }
|
|
1168
|
+
end
|
|
1169
|
+
end
|
|
1170
|
+
end
|