@@ -122,6 +122,65 @@ def mat_mult(mat_a: list[list[float]], mat_b: list[list[float]]) -> list[list[fl
122122 return result
123123
124124
125+ def compute_min_z (body : ET .Element , parent_transform : list [list [float ]] | None = None ) -> float :
126+ """Recursively computes the minimum Z value in the world frame.
127+
128+ This is used to compute the starting height of the robot.
129+
130+ Args:
131+ body: The current body element.
132+ parent_transform: The transform of the parent body.
133+
134+ Returns:
135+ The minimum Z value in the world frame.
136+ """
137+ if parent_transform is None :
138+ parent_transform = [
139+ [1.0 , 0.0 , 0.0 , 0.0 ],
140+ [0.0 , 1.0 , 0.0 , 0.0 ],
141+ [0.0 , 0.0 , 1.0 , 0.0 ],
142+ [0.0 , 0.0 , 0.0 , 1.0 ],
143+ ]
144+ pos_str : str = body .attrib .get ("pos" , "0 0 0" )
145+ quat_str : str = body .attrib .get ("quat" , "1 0 0 0" )
146+ body_tf : list [list [float ]] = mat_mult (parent_transform , build_transform (pos_str , quat_str ))
147+ local_min_z : float = float ("inf" )
148+
149+ for child in body :
150+ if child .tag == "geom" :
151+ gpos_str : str = child .attrib .get ("pos" , "0 0 0" )
152+ gquat_str : str = child .attrib .get ("quat" , "1 0 0 0" )
153+ geom_tf : list [list [float ]] = build_transform (gpos_str , gquat_str )
154+ total_tf : list [list [float ]] = mat_mult (body_tf , geom_tf )
155+
156+ # The translation part of T_total is in column 3.
157+ z : float = total_tf [2 ][3 ]
158+ geom_type : str = child .attrib .get ("type" , "" )
159+ if geom_type == "box" :
160+ size_vals : list [float ] = list (map (float , child .attrib .get ("size" , "0 0 0" ).split ()))
161+ half_height : float = size_vals [2 ] if len (size_vals ) >= 3 else 0.0
162+ candidate : float = z - half_height
163+ elif geom_type == "cylinder" :
164+ size_vals = list (map (float , child .attrib .get ("size" , "0 0" ).split ()))
165+ half_length : float = size_vals [1 ] if len (size_vals ) >= 2 else 0.0
166+ candidate = z - half_length
167+ elif geom_type == "sphere" :
168+ r = float (child .attrib .get ("size" , "0" ))
169+ candidate = z - r
170+ elif geom_type == "mesh" :
171+ candidate = z
172+ else :
173+ candidate = z
174+
175+ local_min_z = min (candidate , local_min_z )
176+
177+ elif child .tag == "body" :
178+ child_min : float = compute_min_z (child , body_tf )
179+ local_min_z = min (child_min , local_min_z )
180+
181+ return local_min_z
182+
183+
125184def add_compiler (root : ET .Element ) -> None :
126185 """Add a compiler element to the MJCF root.
127186
@@ -786,6 +845,18 @@ def build_body(
786845 robot_body = build_body (root_link_name , None , actuator_joints )
787846 if robot_body is None :
788847 raise ValueError ("Failed to build robot body" )
848+
849+ # Gets the minimum z coordinate of the robot body.
850+ min_z : float = compute_min_z (robot_body )
851+ computed_offset : float = - min_z + metadata .height_offset
852+ logger .info ("Auto-detected base offset: %s (min z = %s)" , computed_offset , min_z )
853+
854+ # Moves the robot body to the computed offset.
855+ body_pos = robot_body .attrib .get ("pos" , "0 0 0" )
856+ body_pos = [float (x ) for x in body_pos .split ()]
857+ body_pos [2 ] += computed_offset
858+ robot_body .attrib ["pos" ] = " " .join (f"{ x :.8f} " for x in body_pos )
859+
789860 robot_body .attrib ["childclass" ] = ROBOT_CLASS
790861 worldbody .append (robot_body )
791862
0 commit comments