From d258d5e152d209eb0200412ae17d6e441e488274 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Thu, 28 Mar 2024 11:32:44 -0700 Subject: [PATCH] Check all edge pairs in convex-convex. PiperOrigin-RevId: 620004680 Change-Id: I5d75c0fd21ed3d3e420eed762cfc5a7e04896032 --- doc/mjx.rst | 5 +- mjx/mujoco/mjx/_src/collision_base.py | 6 +- mjx/mujoco/mjx/_src/collision_convex.py | 168 ++++++++---------- mjx/mujoco/mjx/_src/collision_driver.py | 14 +- mjx/mujoco/mjx/_src/collision_driver_test.py | 44 ++++- mjx/mujoco/mjx/_src/mesh.py | 29 ++- mjx/mujoco/mjx/_src/mesh_test.py | 17 +- mjx/mujoco/mjx/_src/types.py | 4 +- .../mjx/test_data/shadow_hand/right_hand.xml | 10 +- 9 files changed, 149 insertions(+), 148 deletions(-) diff --git a/doc/mjx.rst b/doc/mjx.rst index 330336ee..b53708f6 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -299,8 +299,9 @@ Collisions between large meshes SAT works well for smaller meshes but suffers in both runtime and memory for larger meshes. For - collisions with convex meshes, the convex decompositon of the mesh should have - roughly **200 vertices or less** for reasonable performance. + collisions with convex meshes and primitives, the convex decompositon of the mesh should have + roughly **200 vertices or less** for reasonable performance. For convex-convex collisions, + the convex mesh should have roughly **fewer than 32 vertices**. With careful tuning, MJX can simulate scenes with mesh collisions -- see the MJX `shadow hand `__ diff --git a/mjx/mujoco/mjx/_src/collision_base.py b/mjx/mujoco/mjx/_src/collision_base.py index cb6293da..423200ac 100644 --- a/mjx/mujoco/mjx/_src/collision_base.py +++ b/mjx/mujoco/mjx/_src/collision_base.py @@ -50,10 +50,10 @@ class GeomInfo(PyTreeNode): size: jax.Array face: Optional[jax.Array] = None vert: Optional[jax.Array] = None - edge: Optional[jax.Array] = None + edge_dir: Optional[jax.Array] = None facenorm: Optional[jax.Array] = None - face_edge: Optional[jax.Array] = None - face_edge_normal: Optional[jax.Array] = None + edge: Optional[jax.Array] = None + edge_face_normal: Optional[jax.Array] = None class SolverParams(PyTreeNode): diff --git a/mjx/mujoco/mjx/_src/collision_convex.py b/mjx/mujoco/mjx/_src/collision_convex.py index 26eb5e8a..1e501097 100644 --- a/mjx/mujoco/mjx/_src/collision_convex.py +++ b/mjx/mujoco/mjx/_src/collision_convex.py @@ -336,8 +336,8 @@ def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact: cap_closest_pt, ) - edge = jp.take(convex.vert, convex.face_edge, axis=0).reshape(-1, 2, 3) - face_edge_normal = convex.face_edge_normal.reshape((-1, 2, 3)) # pytype: disable=attribute-error + edge = jp.take(convex.vert, convex.edge, axis=0) + edge_face_normal = convex.edge_face_normal # pytype: disable=attribute-error res = jax.vmap(get_edge_axis)(edge.reshape(-1, 2, 3)) e_idx = jp.abs(res[0]).argmin() @@ -349,8 +349,8 @@ def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact: cap_closest_pt, ) = jax.tree_map(lambda x, i=e_idx: jp.take(x, i, axis=0), res) - face_edge_normals = face_edge_normal[e_idx] - edge_voronoi_front = ((face_edge_normals @ edge_axis) < 0).all() + edge_face_normals = edge_face_normal[e_idx] + edge_voronoi_front = ((edge_face_normals @ edge_axis) < 0).all() shallow = ~degenerate_edge_dir & edge_voronoi_front edge_penetration = jp.where(shallow, cap.size[0] - edge_dist, -1) @@ -630,8 +630,9 @@ def _sat_bruteforce( We return both the edge and face contacts. Valid contacts can be checked with dist < 0. Resulting edge contacts should be preferred over face contacts. - This method checks all unique edge-pairs and is thus costly to run over large - meshes, but is more performant for smaller meshes (boxes, tetrahedra, etc.). + This method checks all separating axes via a brute force support function, and + is thus costly to run over large meshes, but is more performant for smaller + meshes (boxes, tetrahedra, etc.). Args: faces_a: Faces for hull A. @@ -731,7 +732,7 @@ def _arcs_intersect( return (cba * dba < 0) & (adc * bdc < 0) & (cba * bdc > 0) -def _sat_approx( +def _sat_gaussmap( centroid_a: jax.Array, faces_a: jax.Array, faces_b: jax.Array, @@ -739,23 +740,16 @@ def _sat_approx( vertices_b: jax.Array, normals_a: jax.Array, normals_b: jax.Array, - face_edges_a: jax.Array, - face_edges_b: jax.Array, - face_edge_normals_a: jax.Array, - face_edge_normals_b: jax.Array, + edges_a: jax.Array, + edges_b: jax.Array, + edge_face_normals_a: jax.Array, + edge_face_normals_b: jax.Array, ) -> Tuple[jax.Array, jax.Array, jax.Array]: """Runs the Separating Axis Test for a pair of hulls. - Runs the separating axis test for all faces. After obtaining a reference - and incident face, tests edge separating axes via edge intersections - on gauss maps. - - Certain meshes can have nearly parallel coplanar faces that are not merged. - Thus reference/incident faces can be non-overlapping, and face contacts will - not get generated. Since we only check edge separating axes on - reference/incident faces, the correct edge separating axes may also be - missing. We mitigate this issue by checking nearly coplanar anti-parallel - reference/incident faces for support, hence why this method is "approximate". + Runs the separating axis test for all faces. Tests edge separating axes via + edge intersections on gauss maps for all edge pairs. h/t to Dirk Gregorius + for the implementation details and gauss map trick. Args: centroid_a: Centroid of hull A. @@ -765,10 +759,10 @@ def _sat_approx( vertices_b: Vertices for hull B. normals_a: Normal vectors for hull A faces. normals_b: Normal vectors for hull B faces. - face_edges_a: Edges for faces in hull A. - face_edges_b: Edges for faces in hull B. - face_edge_normals_a: Edge normals for faces in hull A. - face_edge_normals_b: Edge normals for faces in hull B. + edges_a: Edges for hull A. + edges_b: Edges for hull B. + edge_face_normals_a: Face normals for edges in hull A. + edge_face_normals_b: Face normals for edges in hull B. Returns: tuple of dist, pos, and normal @@ -816,75 +810,61 @@ def _sat_approx( -best_axis, ) - # Handle edge separating axes by checking edge pairs on the reference and - # incident faces. In principle, the correct edge-edge separating axis can be - # created from edge pairs on the reference and incident faces. - # first get the edge directions and face edge normals - face_edges_a = face_edges_a[face_a_idx] - v_norm = jax.vmap(math.normalize) - face_edges_dir_a = v_norm(face_edges_a[:, 0] - face_edges_a[:, 1]) - face_edges_b = face_edges_b[face_b_idx] - face_edges_dir_b = v_norm(face_edges_b[:, 0] - face_edges_b[:, 1]) - face_edge_normals_a = face_edge_normals_a[face_a_idx] - face_edge_normals_b = face_edge_normals_b[face_b_idx] - - # scatter to all edge pairs - edge_idx_a = jp.repeat( - jp.arange(face_edges_a.shape[0]), face_edges_b.shape[0] + # Handle edge separating axes by checking all edge pairs. + a_idx = jp.tile(jp.arange(edges_a.shape[0]), reps=edges_b.shape[0]) + b_idx = jp.repeat( + jp.arange(edges_b.shape[0]), repeats=edges_a.shape[0], axis=0 + ) + normal_a_1 = edge_face_normals_a[a_idx, 0] + normal_a_2 = edge_face_normals_a[a_idx, 1] + normal_b_1 = edge_face_normals_b[b_idx, 0] + normal_b_2 = edge_face_normals_b[b_idx, 1] + is_minkowski_face = jax.vmap(_arcs_intersect)( + normal_a_1, normal_a_2, -normal_b_1, -normal_b_2 ) - edge_idx_b = jp.tile(jp.arange(face_edges_b.shape[0]), face_edges_a.shape[0]) - face_edges_dir_a = face_edges_dir_a[edge_idx_a] - face_edges_dir_b = face_edges_dir_b[edge_idx_b] - face_edges_pt_a = face_edges_a[:, 0][edge_idx_a] - face_edges_pt_b = face_edges_b[:, 0][edge_idx_b] - face_edge_normals_a = face_edge_normals_a[edge_idx_a] - face_edge_normals_b = face_edge_normals_b[edge_idx_b] - @jax.vmap + # get distances + edge_a_dir = jax.vmap(math.normalize)(edges_a[:, 0] - edges_a[:, 1])[a_idx] + edge_b_dir = jax.vmap(math.normalize)(edges_b[:, 0] - edges_b[:, 1])[b_idx] + edges_a, edges_b = edges_a[a_idx], edges_b[b_idx] + edge_a_pt, edge_a_pt_2 = edges_a[:, 0], edges_a[:, 1] + edge_b_pt, edge_b_pt_2 = edges_b[:, 0], edges_b[:, 1] + def get_normals(a_dir, a_pt, b_dir): - edge_axis = math.normalize(jp.cross(a_dir, b_dir)) + edge_axis = jp.cross(a_dir, b_dir) + degenerate_edge_axis = jp.sum(edge_axis**2) < 1e-6 + edge_axis = math.normalize(edge_axis) # correct normal to point from a to b, object b is at the origin sign = jp.where(jp.dot(edge_axis, a_pt - centroid_a) > 0.0, 1.0, -1.0) - return edge_axis * sign + return edge_axis * sign, degenerate_edge_axis - edge_axes = get_normals(face_edges_dir_a, face_edges_pt_a, face_edges_dir_b) - edge_dist = jax.vmap(jp.dot)(edge_axes, face_edges_pt_b - face_edges_pt_a) - # handle degenerate axes - edge_dist = jp.where((edge_axes**2).sum(axis=1) < 1e-6, 1e6, edge_dist) - # ensure edges create a minkowski face by testing intersection on gauss maps - is_minkowski_face = jax.vmap(_arcs_intersect)( - face_edge_normals_a[:, 0], - face_edge_normals_a[:, 1], - -face_edge_normals_b[:, 0], - -face_edge_normals_b[:, 1], - ) - edge_dist = jp.where(is_minkowski_face, edge_dist, 1e6) - edge_dist = jp.where(edge_dist > 0, -1e6, edge_dist) + edge_axes, degenerate_edge_axes = jax.vmap(get_normals)( + edge_a_dir, edge_a_pt, edge_b_dir) + edge_dist = jax.vmap(jp.dot)(edge_axes, edge_b_pt - edge_a_pt) + # handle degenerate axis + edge_dist = jp.where(degenerate_edge_axes, -jp.inf, edge_dist) + # ensure edges create minkowski face + edge_dist = jp.where(is_minkowski_face, edge_dist, -jp.inf) best_edge_idx = edge_dist.argmax() best_edge_dist = edge_dist[best_edge_idx] - # prefer edge over face contacts as long as we have a valid edge contact - is_edge_contact = (best_edge_dist > dist.min() + 1e-6) & (best_edge_dist < 0) - normal = jp.where(is_edge_contact, edge_axes[best_edge_idx], normal) - dist = jp.where(is_edge_contact, jp.array([best_edge_dist, 1, 1, 1]), dist) - - # A failure mode occurs if faces are very narrow and nearly parallel - # (i.e. faces did not get merged properly as coplanar faces). The face - # contacts will be empty since the reference/incident faces may not overlap. - # An edge-edge separating axis will not be found, since the reference and - # incident faces will also not overlap or necessarily create a minkowski face. - # Thus, we approximate the contact for anti-parallel faces with support. - anti_parallel = incident_face_norm.dot(ref_face_norm) < -0.97 - dist = dist.at[0].set( - jp.where( - anti_parallel - & ~is_face_separating - & (dist > 0).all() - & ~is_edge_contact, - -support[best_idx], - dist[0], - ) + is_edge_contact = jp.where( + dist.max() < 0, best_edge_dist > dist.max() - 1e-6, + (best_edge_dist < 0) & ~jp.isinf(best_edge_dist) ) + is_edge_contact = is_edge_contact & ~is_face_separating + normal = jp.where(is_edge_contact, edge_axes[best_edge_idx], normal) + dist = jp.where( + is_edge_contact, + jp.array([best_edge_dist, 1, 1, 1]), + dist, + ) + a_closest, b_closest = math.closest_segment_to_segment_points( + edge_a_pt[best_edge_idx], edge_a_pt_2[best_edge_idx], + edge_b_pt[best_edge_idx], edge_b_pt_2[best_edge_idx]) + pos = jp.where( + is_edge_contact, + jp.tile(0.5 * (a_closest + b_closest), (4, 1)), pos) return dist, pos, normal @@ -924,15 +904,15 @@ def convex_convex(c1: GeomInfo, c2: GeomInfo) -> Contact: unique_edges1 = jp.take(vertices1, c1.edge, axis=0) unique_edges2 = jp.take(vertices2, c2.edge, axis=0) - face_edges1 = jp.take(vertices1, c1.face_edge, axis=0) - face_edges2 = jp.take(vertices2, c2.face_edge, axis=0) + edges1 = jp.take(vertices1, c1.edge, axis=0) + edges2 = jp.take(vertices2, c2.edge, axis=0) - face_edge_normals1 = c1.face_edge_normal @ to_local_mat.T - face_edge_normals2 = c2.face_edge_normal + edge_face_normals1 = c1.edge_face_normal @ to_local_mat.T + edge_face_normals2 = c2.edge_face_normal enable_bruteforce = ( unique_edges1.shape[0] * unique_edges2.shape[0] - < face_edges1[0].shape[0] * face_edges2[0].shape[0] + < edges1[0].shape[0] * edges2[0].shape[0] ) if enable_bruteforce: dist, pos, normal = _sat_bruteforce( @@ -946,7 +926,7 @@ def convex_convex(c1: GeomInfo, c2: GeomInfo) -> Contact: unique_edges2, ) else: - dist, pos, normal = _sat_approx( + dist, pos, normal = _sat_gaussmap( to_local_pos, faces1, faces2, @@ -954,10 +934,10 @@ def convex_convex(c1: GeomInfo, c2: GeomInfo) -> Contact: vertices2, normals1, normals2, - face_edges1, - face_edges2, - face_edge_normals1, - face_edge_normals2, + edges1, + edges2, + edge_face_normals1, + edge_face_normals2, ) # Go back to world frame. diff --git a/mjx/mujoco/mjx/_src/collision_driver.py b/mjx/mujoco/mjx/_src/collision_driver.py index 3b70990c..8fa56612 100644 --- a/mjx/mujoco/mjx/_src/collision_driver.py +++ b/mjx/mujoco/mjx/_src/collision_driver.py @@ -230,20 +230,20 @@ def _pair_info( info = info.replace( face=jp.stack([m.geom_convex_face[i] for i in geom]), vert=jp.stack([m.geom_convex_vert[i] for i in geom]), - edge=jp.stack([m.geom_convex_edge_dir[i] for i in geom]), + edge_dir=jp.stack([m.geom_convex_edge_dir[i] for i in geom]), facenorm=jp.stack([m.geom_convex_facenormal[i] for i in geom]), - face_edge_normal=jp.stack( - [m.geom_convex_face_edge_normal[i] for i in geom] + edge=jp.stack([m.geom_convex_edge[i] for i in geom]), + edge_face_normal=jp.stack( + [m.geom_convex_edge_face_normal[i] for i in geom] ), - face_edge=jp.stack([m.geom_convex_face_edge[i] for i in geom]), ) in_axes = in_axes.replace( face=0, vert=0, - edge=0, + edge_dir=0, facenorm=0, - face_edge=0, - face_edge_normal=0, + edge=0, + edge_face_normal=0, ) return info, in_axes diff --git a/mjx/mujoco/mjx/_src/collision_driver_test.py b/mjx/mujoco/mjx/_src/collision_driver_test.py index 0093f7f3..acb52e12 100644 --- a/mjx/mujoco/mjx/_src/collision_driver_test.py +++ b/mjx/mujoco/mjx/_src/collision_driver_test.py @@ -527,7 +527,7 @@ class ConvexTest(absltest.TestCase): """ def test_convex_convex(self): - """Tests generic convex-convex collision via _sat_approx.""" + """Tests generic convex-convex collision via _sat_gaussmap.""" directory = epath.resource_path('mujoco.mjx') assets = { 'meshes/dodecahedron.stl': ( @@ -542,6 +542,44 @@ class ConvexTest(absltest.TestCase): np.testing.assert_array_less(0, c.dist[1:]) np.testing.assert_array_almost_equal(c.frame[0, 0], np.array([0, 0, 1])) + _CONVEX_CONVEX_THIN = """ + + + + + + + + + + + + + + + + """ + + def test_convex_convex_edge(self): + """Tests convex-convex collisions with edge contact via _sat_gaussmap.""" + _, dx = _collide(self._CONVEX_CONVEX_THIN) + c = dx.contact + + # Only one contact point for an edge contact. + self.assertLess(c.dist[0], 0) + np.testing.assert_array_less(0, c.dist[1:]) + np.testing.assert_array_almost_equal(c.frame[0, 0], np.array([0, 0, 1])) + np.testing.assert_array_almost_equal( + c.pos[0], np.array([0, 2, 1.3155]), decimal=5) + + _, dx = _collide( + self._CONVEX_CONVEX_THIN.replace( + 'pos="0.0 2.0 0.35"', 'pos="0.0 2.0 0"')) + c = dx.contact + self.assertTrue((c.dist > 0).all()) + class BodyPairFilterTest(absltest.TestCase): """Tests that certain body pairs get filtered.""" @@ -647,11 +685,11 @@ class NconTest(parameterized.TestCase): m.numeric_data[m.numeric_adr[i]] = -1 ncon = collision_driver.ncon(m) - self.assertEqual(ncon, 307) + self.assertEqual(ncon, 98) mx = mjx.put_model(m) ncon = collision_driver.ncon(mx) - self.assertEqual(ncon, 307) + self.assertEqual(ncon, 98) class TopKContactTest(absltest.TestCase): diff --git a/mjx/mujoco/mjx/_src/mesh.py b/mjx/mujoco/mjx/_src/mesh.py index 13af828f..39151edf 100644 --- a/mjx/mujoco/mjx/_src/mesh.py +++ b/mjx/mujoco/mjx/_src/mesh.py @@ -49,8 +49,8 @@ _DERIVED_ARGS = [ 'geom_convex_vert', 'geom_convex_edge_dir', 'geom_convex_facenormal', - 'geom_convex_face_edge', - 'geom_convex_face_edge_normal', + 'geom_convex_edge', + 'geom_convex_edge_face_normal', ] DERIVED = {(Model, d) for d in _DERIVED_ARGS} @@ -114,7 +114,7 @@ def _get_unique_edge_dir(vert: np.ndarray, face: np.ndarray) -> np.ndarray: return edges[unique_edge_idx] -def _get_face_edge_normals( +def _get_edge_normals( face: np.ndarray, face_norm: np.ndarray ) -> Tuple[np.ndarray, np.ndarray]: """Returns face edges and face edge normals.""" @@ -133,7 +133,7 @@ def _get_face_edge_normals( continue edge_map_list[tuple(face_edge_flat[i])].append(edge_face_norm[i]) - edge_map = {} + edges, edge_face_normals = [], [] for k, v in edge_map_list.items(): v = np.array(v) if len(v) > 2: @@ -145,19 +145,10 @@ def _get_face_edge_normals( # and face vertices were down sampled. In either case, we ignore these # edges. continue - edge_map[k] = v + edges.append(k) + edge_face_normals.append(v) - # for each face, list the edge normals - face_edge_normal = [] - for face_idx in range(face_edge.shape[0]): - normals = [] - for edge in face_edge[face_idx]: - k = tuple(edge) - normals.append(edge_map.get(k, np.zeros((2, 3)))) - face_edge_normal.append(np.array(normals)) - face_edge_normal = np.array(face_edge_normal) - - return face_edge, face_edge_normal + return np.array(edges), np.array(edge_face_normals) def _convex_hull_2d(points: np.ndarray, normal: np.ndarray) -> np.ndarray: @@ -297,15 +288,15 @@ def _geom_mesh_kwargs( vert = np.array(tm_convex.vertices) face = _merge_coplanar(tm_convex, mesh_info) facenormal = _get_face_norm(vert, face) - face_edge, face_edge_normal = _get_face_edge_normals(face, facenormal) + edge, edge_face_normal = _get_edge_normals(face, facenormal) return { 'geom_convex_face': vert[face], 'geom_convex_face_vert_idx': face, 'geom_convex_vert': vert, 'geom_convex_edge_dir': _get_unique_edge_dir(vert, face), 'geom_convex_facenormal': facenormal, - 'geom_convex_face_edge': face_edge, - 'geom_convex_face_edge_normal': face_edge_normal, + 'geom_convex_edge': edge, + 'geom_convex_edge_face_normal': edge_face_normal, } diff --git a/mjx/mujoco/mjx/_src/mesh_test.py b/mjx/mujoco/mjx/_src/mesh_test.py index 233f3baa..266a81f0 100644 --- a/mjx/mujoco/mjx/_src/mesh_test.py +++ b/mjx/mujoco/mjx/_src/mesh_test.py @@ -80,7 +80,7 @@ class GeomMeshKwargsTest(absltest.TestCase): self.assertEqual(h['geom_convex_facenormal'].shape, (5, 3)) # face edges - edges = np.concatenate(h['geom_convex_face_edge']) + edges = h['geom_convex_edge'] edges = np.vectorize(map_.get)(edges) mask = edges[:, 0] != edges[:, 1] edges = edges[mask] @@ -92,27 +92,18 @@ class GeomMeshKwargsTest(absltest.TestCase): edges, np.array([ [0, 2], - [0, 2], - [0, 3], [0, 3], [0, 4], - [0, 4], - [1, 2], [1, 2], [1, 3], - [1, 3], - [1, 4], [1, 4], [2, 4], - [2, 4], - [3, 4], [3, 4], ]), ) # face edge normals - edge_normal = h['geom_convex_face_edge_normal'] - edge_normal = np.concatenate(edge_normal) + edge_normal = h['geom_convex_edge_face_normal'] edge_normal = edge_normal[mask] edge_normal = np.take_along_axis( edge_normal, sort_col_idx[..., None], axis=1 @@ -120,8 +111,8 @@ class GeomMeshKwargsTest(absltest.TestCase): edge_normal = edge_normal[sort_row_idx] edge_normal_02 = np.array([[0.4472136, -0.0, 0.89442719], [-1.0, 0.0, 0.0]]) np.testing.assert_array_almost_equal( - edge_normal[:2], - np.array([edge_normal_02, edge_normal_02]), + edge_normal[:1], + np.array([edge_normal_02]), ) diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 92b04a72..97870a40 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -543,8 +543,8 @@ class Model(PyTreeNode): geom_convex_vert: List[Optional[jax.Array]] geom_convex_edge_dir: List[Optional[jax.Array]] geom_convex_facenormal: List[Optional[jax.Array]] - geom_convex_face_edge: List[Optional[jax.Array]] - geom_convex_face_edge_normal: List[Optional[jax.Array]] + geom_convex_edge: List[Optional[jax.Array]] + geom_convex_edge_face_normal: List[Optional[jax.Array]] pair_solref: jax.Array pair_solreffriction: jax.Array pair_solimp: jax.Array diff --git a/mjx/mujoco/mjx/test_data/shadow_hand/right_hand.xml b/mjx/mujoco/mjx/test_data/shadow_hand/right_hand.xml index 13bc5435..ba1a2f34 100644 --- a/mjx/mujoco/mjx/test_data/shadow_hand/right_hand.xml +++ b/mjx/mujoco/mjx/test_data/shadow_hand/right_hand.xml @@ -156,7 +156,7 @@ diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/> - + @@ -181,7 +181,7 @@ diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/> - + @@ -206,7 +206,7 @@ diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/> - + @@ -236,7 +236,7 @@ diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/> - + @@ -266,7 +266,7 @@ diaginertia="2.37794e-06 2.27794e-06 1e-06"/> - +