Simulating a 3D CAD model in a physics engine like MuJoCo can be a powerful way to test designs in a virtual environment. However, MuJoCo doesn’t directly understand CAD formats like STEP (STP) files — it needs STL meshes and an XML file to define the simulation. In this blog, I’ll walk you through the process of converting a STEP file into a MuJoCo XML file using Python, focusing on the key steps, the reasoning behind them, and a real-world issue we encountered with shells and solids. Let’s get started!
MuJoCo is a physics engine used for robotics, animation, and engineering simulations. It requires:
A STEP file, on the other hand, is a standard CAD format (ISO 10303) that stores precise 3D geometry using mathematical definitions (e.g., surfaces, solids). To use it in MuJoCo, we need to:
This process lets us take a CAD design — like our arch bridge — and simulate its physical behavior, such as how it handles stress or movement.
Before diving in, let’s clarify some terms you’ll encounter in a STEP file:
In our case, the ACDC_arch.stp file represents an arch bridge, which we expected to have 14 distinct segments (parts). However, as we’ll see, the STEP file’s structure caused an issue that we needed to resolve.
Install the Python libraries:
pip install pythonocc-core numpy stl
Here’s the high-level process to convert the STEP file to MuJoCo XML, along with why each step matters:
Extract Parts from the STEP File:
Convert Parts to STL Files:
Generate the MuJoCo XML File:
Here’s the simplified code to achieve this, focusing on the main ideas.
We use pythonocc-core to read the STEP file, extract solids, and convert them to STL files.
import os
from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.TopExp import TopExp_Explorer, TopAbs_SOLID
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
from OCC.Core.StlAPI import StlAPI_Writer
from OCC.Core.TopoDS import topods
def extract_and_convert_stp_to_stl(stp_file_path, output_dir):
os.makedirs(output_dir, exist_ok=True)
step_reader = STEPControl_Reader()
step_reader.ReadFile(stp_file_path)
step_reader.TransferRoots()
all_solids = []
part_info = []
for i in range(1, step_reader.NbRootsForTransfer() + 1):
shape = step_reader.Shape(i)
explorer = TopExp_Explorer(shape, TopAbs_SOLID)
while explorer.More():
solid = topods.Solid(explorer.Current())
if solid not in all_solids:
all_solids.append(solid)
part_info.append({'id': len(all_solids), 'name': f'part_{len(all_solids)}'})
explorer.Next()
print(f"Detected {len(all_solids)} solids in the STEP file")
successful_conversions = 0
for i, solid in enumerate(all_solids):
try:
mesh = BRepMesh_IncrementalMesh(solid, 0.01)
mesh.Perform()
if not mesh.IsDone():
continue
stl_writer = StlAPI_Writer()
stl_file = os.path.join(output_dir, f"part_{i+1}.stl")
stl_writer.Write(solid, stl_file)
successful_conversions += 1
print(f"Created {stl_file}")
except Exception as e:
print(f"Error processing part_{i+1}: {e}")
return successful_conversions, part_info[:successful_conversions]
We create an XML file that references the STL files and sets up the simulation.
import xml.etree.ElementTree as ET
import xml.dom.minidom as minidom
import os
def create_mujoco_xml(part_info, output_file, mesh_dir):
root = ET.Element("mujoco", model="arch_bridge")
ET.SubElement(root, "option", gravity="0 0 -9.81")
asset = ET.SubElement(root, "asset")
stl_files = sorted([f for f in os.listdir(mesh_dir) if f.endswith('.stl')])
for i, stl_file in enumerate(stl_files):
ET.SubElement(asset, "mesh", name=f"mesh_{i}", file=f"meshes/bridge/{stl_file}", scale="0.001 0.001 0.001")
worldbody = ET.SubElement(root, "worldbody")
bridge_body = ET.SubElement(worldbody, "body", name="bridge", pos="0 0 0.15")
for i in range(len(stl_files)):
ET.SubElement(bridge_body, "geom", name=f"part_{i+1}", type="mesh", mesh=f"mesh_{i}")
pretty_xml = minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
with open(output_file, 'w') as f:
f.write(pretty_xml)
print(f"Created MuJoCo XML file: {output_file}")
Tie it all together in a main() function.
def main():
stp_file_path = "assembly/assets/ACDC_arch.stp"
mesh_dir = "assembly/assets/meshes/bridge"
output_xml_file = "assembly/assets/arch_bridge.xml"
num_parts, part_info = extract_and_convert_stp_to_stl(stp_file_path, mesh_dir)
if num_parts == 0:
print("Error: No parts converted. Aborting.")
return
create_mujoco_xml(part_info, output_xml_file, mesh_dir)
print(f"Conversion complete with {num_parts} parts!")
While working on our test file, we hit a snag: the bridge visually has 14 segments, but the code only detected 13 solids. This mismatch caused the original code to add a placeholder part, which didn’t reflect the true design.
To diagnose the issue, we added logging to inspect the STEP file’s structure:
However, upon re-evaluating the visual model, we confirmed the bridge indeed has 14 segments. The discrepancy arose because the four shells were meant to be four separate solids, not one composite solid.
We used FreeCAD to fix the STEP file:
After these changes, the code detected 14 solids, converted them to 14 STL files, and generated the MuJoCo XML without placeholders.
This process bridges the gap between CAD and physics simulation:
Whether you’re simulating a bridge, a robot, or any CAD model, this workflow lets you bring your designs into MuJoCo for testing. Try it with your own STEP file, and if you run into issues like shells vs. solids, FreeCAD can help you resolve them!