1717from ..core .types import Axis , AxisType , Sequence , Transform , vec10
1818from ..geometry import GeoType , Mesh , ShapeFlags , compute_inertia_shape
1919from ..geometry .types import Heightfield
20- from ..geometry .utils import compute_aabb , compute_inertia_box_mesh
20+ from ..geometry .utils import compute_aabb , compute_inertia_box_mesh , remesh_convex_hull
2121from ..sim import JointTargetMode , JointType , ModelBuilder
2222from ..sim .model import Model
2323from ..solvers .mujoco import SolverMuJoCo
@@ -508,19 +508,106 @@ def resolve_element_attrib(element, tag: str, ambient_defaults: dict | None = No
508508 hfield_assets = {}
509509 for asset in root .findall ("asset" ):
510510 for mesh in asset .findall ("mesh" ):
511- if "file" in mesh .attrib :
512- fname = os .path .join (mesh_dir , mesh .attrib ["file" ])
511+ mesh_attrib = resolve_element_attrib (mesh , "mesh" )
512+ mesh_file = mesh_attrib .get ("file" )
513+ mesh_name = mesh_attrib .get ("name" )
514+ mesh_scale = np .array (mesh_attrib .get ("scale" , "1.0 1.0 1.0" ).split (), dtype = np .float32 )
515+ maxhullvert = int (mesh_attrib .get ("maxhullvert" , str (mesh_maxhullvert )))
516+
517+ if mesh_file :
518+ fname = os .path .join (mesh_dir , mesh_file )
513519 # handle stl relative paths
514520 if not os .path .isabs (fname ):
515521 fname = os .path .abspath (os .path .join (mjcf_dirname , fname ))
516- # resolve mesh element's class defaults
517- mesh_attrib = resolve_element_attrib (mesh , "mesh" )
518- name = mesh .attrib .get ("name" , "." .join (os .path .basename (fname ).split ("." )[:- 1 ]))
519- s = mesh_attrib .get ("scale" , "1.0 1.0 1.0" )
520- s = np .array (s .split (), dtype = np .float32 )
521- # parse maxhullvert attribute, default to mesh_maxhullvert if not specified
522- maxhullvert = int (mesh_attrib .get ("maxhullvert" , str (mesh_maxhullvert )))
523- mesh_assets [name ] = {"file" : fname , "scale" : s , "maxhullvert" : maxhullvert }
522+ name = mesh_name or "." .join (os .path .basename (fname ).split ("." )[:- 1 ])
523+ mesh_assets [name ] = {"file" : fname , "scale" : mesh_scale , "maxhullvert" : maxhullvert }
524+ elif "vertex" in mesh_attrib :
525+ name = mesh_name
526+ if not name :
527+ raise ValueError ("Inline MJCF mesh assets require a name." )
528+ try :
529+ vertices = np .array (mesh_attrib ["vertex" ].split (), dtype = np .float32 )
530+ except ValueError as exc :
531+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid vertex data." ) from exc
532+ if len (vertices ) % 3 != 0 :
533+ raise ValueError (
534+ f"Inline MJCF mesh { name !r} vertex data must contain a multiple of 3 values; "
535+ f"got { len (vertices )} ."
536+ )
537+ if len (vertices ) < 9 :
538+ raise ValueError (f"Inline MJCF mesh { name !r} must contain at least 3 vertices." )
539+ vertices = vertices .reshape (- 1 , 3 )
540+
541+ faces = None
542+ face_data = mesh_attrib .get ("face" , "" ).strip ()
543+ if face_data :
544+ try :
545+ faces = np .array (face_data .split (), dtype = np .int32 )
546+ except ValueError as exc :
547+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid face data." ) from exc
548+ if len (faces ) % 3 != 0 :
549+ raise ValueError (
550+ f"Inline MJCF mesh { name !r} face data must contain a multiple of 3 values; got { len (faces )} ."
551+ )
552+ if np .any (faces < 0 ) or np .any (faces >= len (vertices )):
553+ raise ValueError (f"Inline MJCF mesh { name !r} face data contains an invalid vertex index." )
554+ faces = faces .reshape (- 1 , 3 )
555+
556+ normals = None
557+ if "normal" in mesh_attrib :
558+ try :
559+ normals = np .array (mesh_attrib ["normal" ].split (), dtype = np .float32 )
560+ except ValueError as exc :
561+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid normal data." ) from exc
562+ if len (normals ) != 3 * len (vertices ):
563+ raise ValueError (
564+ f"Inline MJCF mesh { name !r} normal data must contain 3 values per vertex; "
565+ f"got { len (normals )} values for { len (vertices )} vertices."
566+ )
567+ normals = normals .reshape (- 1 , 3 )
568+
569+ texcoords = None
570+ if "texcoord" in mesh_attrib :
571+ try :
572+ texcoords = np .array (mesh_attrib ["texcoord" ].split (), dtype = np .float32 )
573+ except ValueError as exc :
574+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid texcoord data." ) from exc
575+ if len (texcoords ) != 2 * len (vertices ):
576+ raise ValueError (
577+ f"Inline MJCF mesh { name !r} texcoord data must contain 2 values per vertex; "
578+ f"got { len (texcoords )} values for { len (vertices )} vertices."
579+ )
580+ texcoords = texcoords .reshape (- 1 , 2 )
581+
582+ try :
583+ refpos = np .array (mesh_attrib .get ("refpos" , "0 0 0" ).split (), dtype = np .float32 )
584+ except ValueError as exc :
585+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid refpos data." ) from exc
586+ try :
587+ refquat = np .array (mesh_attrib .get ("refquat" , "1 0 0 0" ).split (), dtype = np .float32 )
588+ except ValueError as exc :
589+ raise ValueError (f"Inline MJCF mesh { name !r} has invalid refquat data." ) from exc
590+ if refpos .shape != (3 ,):
591+ raise ValueError (f"Inline MJCF mesh { name !r} refpos must have 3 values." )
592+ if refquat .shape != (4 ,):
593+ raise ValueError (f"Inline MJCF mesh { name !r} refquat must have 4 values." )
594+ refquat_norm = np .linalg .norm (refquat )
595+ if not np .isfinite (refquat_norm ) or refquat_norm == 0.0 :
596+ raise ValueError (f"Inline MJCF mesh { name !r} refquat must be finite and nonzero." )
597+ if not np .all (np .isfinite (refpos )):
598+ raise ValueError (f"Inline MJCF mesh { name !r} refpos must contain only finite values." )
599+ refquat /= refquat_norm
600+
601+ mesh_assets [name ] = {
602+ "vertices" : vertices ,
603+ "faces" : faces ,
604+ "normals" : normals ,
605+ "texcoords" : texcoords ,
606+ "refpos" : refpos ,
607+ "refquat" : refquat ,
608+ "scale" : mesh_scale ,
609+ "maxhullvert" : maxhullvert ,
610+ }
524611 for texture in asset .findall ("texture" ):
525612 tex_name = texture .attrib .get ("name" )
526613 tex_file = texture .attrib .get ("file" )
@@ -575,6 +662,64 @@ def resolve_element_attrib(element, tag: str, ambient_defaults: dict | None = No
575662 "elevation" : elevation_data ,
576663 }
577664
665+ def load_mesh_asset (
666+ mesh_name : str ,
667+ scaling : np .ndarray ,
668+ maxhullvert : int ,
669+ override_color : tuple [float , float , float ] | None = None ,
670+ override_texture : str | None = None ,
671+ ) -> list [Mesh ]:
672+ mesh_asset = mesh_assets [mesh_name ]
673+ if "file" in mesh_asset :
674+ return load_meshes_from_file (
675+ mesh_asset ["file" ],
676+ scale = scaling ,
677+ maxhullvert = maxhullvert ,
678+ override_color = override_color ,
679+ override_texture = override_texture ,
680+ )
681+
682+ refquat = mesh_asset ["refquat" ]
683+ rotation = np .asarray (
684+ wp .quat_to_matrix (wp .quat (refquat [1 ], refquat [2 ], refquat [3 ], refquat [0 ])),
685+ dtype = np .float32 ,
686+ ).reshape (3 , 3 )
687+ vertices = ((mesh_asset ["vertices" ] - mesh_asset ["refpos" ]) @ rotation ) * scaling
688+ faces = mesh_asset ["faces" ]
689+ normals = mesh_asset ["normals" ]
690+ texcoords = mesh_asset ["texcoords" ]
691+ if normals is not None :
692+ normals = (normals @ rotation ) / scaling
693+ lengths = np .linalg .norm (normals , axis = 1 , keepdims = True )
694+ normals = np .divide (normals , lengths , out = np .zeros_like (normals ), where = lengths > 0.0 )
695+
696+ if faces is None :
697+ hull_vertices , faces = remesh_convex_hull (vertices , maxhullvert = maxhullvert )
698+ source_index_by_vertex = {}
699+ for index , vertex in enumerate (vertices ):
700+ source_index_by_vertex .setdefault (tuple (vertex ), index )
701+ source_indices = np .array (
702+ [source_index_by_vertex [tuple (vertex )] for vertex in hull_vertices ],
703+ dtype = np .int32 ,
704+ )
705+ vertices = hull_vertices
706+ if normals is not None :
707+ normals = normals [source_indices ]
708+ if texcoords is not None :
709+ texcoords = texcoords [source_indices ]
710+
711+ return [
712+ Mesh (
713+ vertices ,
714+ faces ,
715+ normals = normals ,
716+ uvs = texcoords ,
717+ maxhullvert = maxhullvert ,
718+ color = override_color ,
719+ texture = override_texture ,
720+ )
721+ ]
722+
578723 axis_xform = wp .transform (wp .vec3 (0.0 ), quat_between_axes (up_axis , builder .up_axis ))
579724 xform = xform * axis_xform
580725
@@ -873,19 +1018,14 @@ def parse_shapes(
8731018 print (f"Warning: mesh asset for fitting not found for { geom_name } , skipping geom" )
8741019 continue
8751020 else :
876- stl_file = mesh_assets [mesh_name ]["file" ]
8771021 if "mesh" in geom_defaults :
8781022 mesh_scale = parse_vec (geom_defaults ["mesh" ], "scale" , mesh_assets [mesh_name ]["scale" ])
8791023 else :
8801024 mesh_scale = mesh_assets [mesh_name ]["scale" ]
8811025 scaling = np .array (mesh_scale ) * scale
8821026 maxhullvert = mesh_assets [mesh_name ].get ("maxhullvert" , mesh_maxhullvert )
8831027
884- m_meshes = load_meshes_from_file (
885- stl_file ,
886- scale = scaling ,
887- maxhullvert = maxhullvert ,
888- )
1028+ m_meshes = load_mesh_asset (mesh_name , scaling , maxhullvert )
8891029 # Combine all sub-meshes into one vertex array for fitting.
8901030 all_vertices = np .concatenate ([m .vertices for m in m_meshes ], axis = 0 )
8911031
@@ -1001,18 +1141,19 @@ def parse_shapes(
10011141 if verbose :
10021142 print (f"Warning: mesh asset { geom_attrib ['mesh' ]} not found, skipping" )
10031143 continue
1004- stl_file = mesh_assets [geom_attrib ["mesh" ]]["file" ]
1005- mesh_scale = mesh_assets [geom_attrib ["mesh" ]]["scale" ]
1144+ mesh_asset = mesh_assets [geom_attrib ["mesh" ]]
1145+ mesh_label = mesh_asset .get ("file" , geom_attrib ["mesh" ])
1146+ mesh_scale = mesh_asset ["scale" ]
10061147 scaling = np .array (mesh_scale ) * scale
10071148 # as per the Mujoco XML reference, ignore geom size attribute
10081149
10091150 # get maxhullvert value from mesh assets
10101151 maxhullvert = mesh_assets [geom_attrib ["mesh" ]].get ("maxhullvert" , mesh_maxhullvert )
10111152
1012- m_meshes = load_meshes_from_file (
1013- stl_file ,
1014- scale = scaling ,
1015- maxhullvert = maxhullvert ,
1153+ m_meshes = load_mesh_asset (
1154+ geom_attrib [ "mesh" ] ,
1155+ scaling ,
1156+ maxhullvert ,
10161157 override_color = material_color ,
10171158 override_texture = texture ,
10181159 )
@@ -1035,7 +1176,7 @@ def parse_shapes(
10351176 for m_mesh in m_meshes :
10361177 if m_mesh .texture is not None and m_mesh .uvs is None :
10371178 if verbose :
1038- print (f"Warning: mesh { stl_file } has a texture but no UVs; texture will be ignored." )
1179+ print (f"Warning: mesh { mesh_label } has a texture but no UVs; texture will be ignored." )
10391180 m_mesh .texture = None
10401181 # Mesh shapes must not use cfg.sdf_*; SDFs are built on the mesh itself.
10411182 mesh_shape_kwargs = dict (shape_kwargs )
0 commit comments