Skip to content

Commit bf13a0c

Browse files
authored
Merge pull request #84 from compas-dev/fix/dae-and-multi-mesh-support
Add support for DAE files and multiple meshes per mesh descriptor
2 parents 40aa0e9 + c66d3c6 commit bf13a0c

File tree

4 files changed

+121
-42
lines changed

4 files changed

+121
-42
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Unreleased
2424

2525
* Use WorldXY's origin as default for robots that are have no parent join on their base
2626
* Fixed parsing of semantics (SRDF) containing nested groups
27+
* Fixed DAE support on ROS File loader
2728

2829
**Deprecated**
2930

docs/backends/files/panda-demo/docker-compose.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ services:
3838
- file_server
3939
- file_server.launch
4040

41+
panda-tf-panda_link0:
42+
image: gramaziokohler/ros-panda-planner:19.04
43+
container_name: panda-tf-panda_link0
44+
environment:
45+
- ROS_HOSTNAME=panda-tf-panda_link0
46+
- ROS_MASTER_URI=http://ros-master:11311
47+
depends_on:
48+
- ros-master
49+
command:
50+
- rosrun
51+
- tf
52+
- static_transform_publisher
53+
- '0'
54+
- '0'
55+
- '0'
56+
- '0'
57+
- '0'
58+
- '0'
59+
- '1'
60+
- world
61+
- panda_link0
62+
- '100'
63+
4164
panda-demo:
4265
image: gramaziokohler/ros-panda-planner:19.04
4366
container_name: panda-demo

src/compas_fab/artists/base.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,34 @@ def create(self, link=None):
8989
link = self.robot.root
9090

9191
for item in itertools.chain(link.visual, link.collision):
92-
if item.geometry.geo:
92+
# NOTE: Currently, shapes assign their meshes to an
93+
# attribute called `geometry`, but this will change soon to `meshes`.
94+
# This code handles the situation in a forward-compatible
95+
# manner. Eventually, this can be simplified to use only `meshes` attr
96+
if hasattr(item.geometry.shape, 'meshes'):
97+
meshes = item.geometry.shape.meshes
98+
else:
99+
meshes = item.geometry.shape.geometry
100+
101+
if meshes:
102+
# Coerce meshes into an iteratable (a tuple if not natively iterable)
103+
if not hasattr(meshes, '__iter__'):
104+
meshes = (meshes,)
105+
93106
color = None
94107
if hasattr(item, 'get_color'):
95108
color = item.get_color()
96-
# create native geometry
97-
item.native_geometry = self.draw_geometry(item.geometry.geo, color)
98-
# transform native geometry based on saved init transform
99-
self.transform(item.native_geometry, item.init_transformation)
109+
110+
native_geometry = []
111+
for mesh in meshes:
112+
# create native geometry
113+
native_mesh = self.draw_geometry(mesh, color)
114+
# transform native geometry based on saved init transform
115+
self.transform(native_mesh, item.init_transformation)
116+
# append to list
117+
native_geometry.append(native_mesh)
118+
119+
item.native_geometry = native_geometry
100120
item.current_transformation = Transformation()
101121

102122
for child_joint in link.joints:
@@ -128,7 +148,8 @@ def scale_link(self, link, transformation):
128148
# Some links have only collision geometry, not visual. These meshes
129149
# have not been loaded.
130150
if item.native_geometry:
131-
self.transform(item.native_geometry, transformation)
151+
for geometry in item.native_geometry:
152+
self.transform(geometry, transformation)
132153

133154
for child_joint in link.joints:
134155
# Recursive call
@@ -152,7 +173,8 @@ def _apply_transformation_on_transformed_link(self, item, transformation):
152173
None
153174
"""
154175
relative_transformation = transformation * item.current_transformation.inverse()
155-
self.transform(item.native_geometry, relative_transformation)
176+
for native_geometry in item.native_geometry:
177+
self.transform(native_geometry, relative_transformation)
156178
item.current_transformation = transformation
157179

158180
def update(self, configuration, names, visual=True, collision=True):
@@ -190,11 +212,14 @@ def draw_visual(self):
190212
"""Draws all visual geometry of the robot."""
191213
for link in self.robot.iter_links():
192214
for item in link.visual:
193-
yield item.native_geometry
215+
if item.native_geometry:
216+
for native_geometry in item.native_geometry:
217+
yield native_geometry
194218

195219
def draw_collision(self):
196220
"""Draws all collision geometry of the robot."""
197221
for link in self.robot.iter_links():
198222
for item in link.collision:
199223
if item.native_geometry:
200-
yield item.native_geometry
224+
for native_geometry in item.native_geometry:
225+
yield native_geometry

src/compas_fab/backends/ros/fileserver_loader.py

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
import logging
77
import os
88
import tempfile
9+
from collections import OrderedDict
910

1011
import roslibpy
1112
from compas.datastructures import Mesh
12-
from compas.datastructures import meshes_join
13+
from compas.datastructures import mesh_transform
1314
from compas.files import XML
15+
from compas.geometry import Transformation
1416
from compas.robots.resources.basic import _get_file_format
1517
from compas.robots.resources.basic import _mesh_import
1618
from compas.utilities import await_callback
19+
from compas.utilities import geometric_key
1720

1821
LOGGER = logging.getLogger('compas_fab.backends.ros')
1922

@@ -61,14 +64,17 @@ class RosFileServerLoader(object):
6164
local_cache_directory : str, optional
6265
Directory name to store the cached files. Only used if
6366
``local_cache`` is ``True``. Defaults to ``~/robot_description``.
67+
precision : float
68+
Defines precision for importing/loading meshes. Defaults to ``compas.PRECISION``.
6469
"""
6570

66-
def __init__(self, ros=None, local_cache=False, local_cache_directory=None):
71+
def __init__(self, ros=None, local_cache=False, local_cache_directory=None, precision=None):
6772
self.robot_name = None
6873
self.schema_prefix = 'package://'
6974
self.ros = ros
7075
self.local_cache_directory = None
7176
self.local_cache_enabled = local_cache
77+
self.precision = precision
7278

7379
if self.local_cache_enabled:
7480
self.local_cache_directory = local_cache_directory or os.path.join(
@@ -184,7 +190,7 @@ def load_mesh(self, url):
184190
185191
Returns
186192
-------
187-
:class:`Mesh`
193+
:class:`Mesh` or list of :class:`Mesh`
188194
Instance of a mesh.
189195
"""
190196
use_local_file = False
@@ -217,61 +223,85 @@ def load_mesh(self, url):
217223
# Nothing to do here, the file will be read by the mesh importer
218224
LOGGER.debug('Loading mesh file %s from local cache dir', local_filename)
219225

220-
return _fileserver_mesh_import(url, local_filename)
226+
return _fileserver_mesh_import(url, local_filename, self.precision)
221227

222228
def _local_mesh_filename(self, url):
223229
return os.path.abspath(os.path.join(self._robot_resource_path, url[len('package://'):]))
224230

225231

226-
def _dae_mesh_importer(filename):
227-
"""This is a very simple implementation of a DAE/Collada parser.
228-
It merges all solids of the DAE file into one mesh, because
229-
several other parts of the framework don't support multi-meshes per file."""
232+
def _dae_mesh_importer(filename, precision):
233+
"""This is a very simple implementation of a DAE/Collada parser."""
230234
dae = XML.from_file(filename)
231235
meshes = []
236+
visual_scenes = dae.root.find('library_visual_scenes')
232237

233-
for mesh_xml in dae.root.findall('.//mesh'):
234-
for triangle_set in mesh_xml.findall('triangles'):
235-
triangle_set_data = triangle_set.find('p').text.split()
238+
for geometry in dae.root.findall('.//geometry'):
239+
mesh_xml = geometry.find('mesh')
240+
mesh_id = geometry.attrib['id']
241+
matrix_node = visual_scenes.find('visual_scene/node/instance_geometry[@url="#{}"]/../matrix'.format(mesh_id))
242+
transform = None
236243

237-
# Parse vertices
238-
vertices_input = triangle_set.find('input[@semantic="VERTEX"]')
239-
vertices_link = mesh_xml.find('vertices[@id="{}"]/input'.format(vertices_input.attrib['source'][1:]))
240-
positions = mesh_xml.find('source[@id="{}"]/float_array'.format(vertices_link.attrib['source'][1:]))
241-
positions = positions.text.split()
244+
if matrix_node is not None:
245+
M = [float(i) for i in matrix_node.text.split()]
242246

243-
vertices = list(map(float, positions[i:i + 3]) for i in range(0, len(positions), 3))
247+
# If it's the identity matrix, then ignore, we don't need to transform it
248+
if M != [1., 0., 0., 0.,
249+
0., 1., 0., 0.,
250+
0., 0., 1., 0.,
251+
0., 0., 0., 1.]:
252+
M = M[0:4], M[4:8], M[8:12], M[12:16]
253+
transform = Transformation.from_matrix(M)
244254

245-
# Parse faces
246-
faces = list(map(int, triangle_set_data[::2])) # Ignore normals (ever second item is normal index)
247-
faces = list(faces[i:i + 3] for i in range(0, len(faces), 3))
255+
triangle_set = mesh_xml.find('triangles')
256+
triangle_set_data = triangle_set.find('p').text.split()
248257

249-
mesh = Mesh.from_vertices_and_faces(vertices, faces)
258+
# Parse vertices
259+
vertices_input = triangle_set.find('input[@semantic="VERTEX"]')
260+
vertices_link = mesh_xml.find('vertices[@id="{}"]/input'.format(vertices_input.attrib['source'][1:]))
261+
positions = mesh_xml.find('source[@id="{}"]/float_array'.format(vertices_link.attrib['source'][1:]))
262+
positions = positions.text.split()
250263

251-
meshes.append(mesh)
264+
vertices = [[float(p) for p in positions[i:i + 3]] for i in range(0, len(positions), 3)]
252265

253-
combined_mesh = meshes_join(meshes)
266+
# Parse faces
267+
faces = [int(f) for f in triangle_set_data[::2]] # Ignore normals (every second item is normal index)
268+
faces = [faces[i:i + 3] for i in range(0, len(faces), 3)]
254269

255-
# from compas.datastructures import mesh_transform
256-
# from compas.geometry import Frame
257-
# from compas.geometry import Transformation
270+
# Rebuild vertices and faces using the same logic that other importers
271+
# use remapping everything based on a selected precision
272+
index_key = OrderedDict()
273+
vertex = OrderedDict()
258274

259-
# former DAE files have yaxis and zaxis swapped
260-
# frame = Frame([0, 0, 0], [1, 0, 0], [0, 0, 1])
261-
# T = Transformation.from_frame(frame)
262-
# mesh_transform(combined_mesh, T)
263-
return combined_mesh
275+
for i, xyz in enumerate(vertices):
276+
key = geometric_key(xyz, precision)
277+
index_key[i] = key
278+
vertex[key] = xyz
264279

280+
key_index = {key: index for index, key in enumerate(vertex)}
281+
index_index = {index: key_index[key] for index, key in iter(index_key.items())}
282+
vertices = [xyz for xyz in iter(vertex.values())]
283+
faces = [[index_index[index] for index in face] for face in faces]
265284

266-
def _fileserver_mesh_import(url, filename):
285+
mesh = Mesh.from_vertices_and_faces(vertices, faces)
286+
287+
if transform:
288+
mesh_transform(mesh, transform)
289+
290+
meshes.append(mesh)
291+
292+
return meshes
293+
294+
295+
def _fileserver_mesh_import(url, filename, precision=None):
267296
"""Internal function that adds primitive support for DAE files
268297
to the _mesh_import function of compas.robots."""
269298
file_extension = _get_file_format(url)
270299

271300
if file_extension == 'dae':
272301
# Magic!
273-
return _dae_mesh_importer(filename)
302+
return _dae_mesh_importer(filename, precision)
274303
else:
304+
# TODO: This _mesh_import should also add support for precision
275305
return _mesh_import(url, filename)
276306

277307

0 commit comments

Comments
 (0)