diff --git a/VirtualRobot/CMakeLists.txt b/VirtualRobot/CMakeLists.txt
index 5f2376482e5bf0c03e57c6a378e8a1fd8c373052..aad579fd4f258acd0f43845b7e7c652d442d07cc 100644
--- a/VirtualRobot/CMakeLists.txt
+++ b/VirtualRobot/CMakeLists.txt
@@ -377,6 +377,7 @@ SET(SOURCES
     XML/mujoco/DummyMassBodySanitizer.cpp
     XML/mujoco/MergingBodySanitizer.cpp
     XML/mujoco/MujocoIO.cpp
+    XML/mujoco/RobotMjcf.cpp
 )
 
 SET(INCLUDES
@@ -600,6 +601,7 @@ SET(INCLUDES
     XML/mujoco/DummyMassBodySanitizer.h
     XML/mujoco/MergingBodySanitizer.h
     XML/mujoco/MujocoIO.h
+    XML/mujoco/RobotMjcf.h
     
 )
 
diff --git a/VirtualRobot/XML/mujoco/RobotMjcf.cpp b/VirtualRobot/XML/mujoco/RobotMjcf.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e6545b5d2bd52e890f06214f4f842fff76222039
--- /dev/null
+++ b/VirtualRobot/XML/mujoco/RobotMjcf.cpp
@@ -0,0 +1,226 @@
+#include "RobotMjcf.h"
+
+#include <boost/shared_ptr.hpp>
+
+#include <VirtualRobot/RobotNodeSet.h>
+
+#include <VirtualRobot/Nodes/RobotNodePrismatic.h>
+#include <VirtualRobot/Nodes/RobotNodeRevolute.h>
+
+
+
+namespace VirtualRobot::mujoco
+{
+
+RobotMjcf::RobotMjcf(RobotPtr robot) : robot(robot)
+{
+    document->setModelName(robot->getName());
+    
+    // Add robot root.
+    robotBody = document->worldbody().addBody(robot->getName(), robot->getName());
+    nodeBodies[robot->getName()] = robotBody;
+}
+
+mjcf::Document& RobotMjcf::getDocument()
+{
+    return *document;
+}
+
+const mjcf::Document& RobotMjcf::getDocument() const
+{
+    return *document;
+}
+
+RobotPtr RobotMjcf::getRobot() const
+{
+    return robot;
+}
+
+void RobotMjcf::setOutputFile(const std::filesystem::path& filePath)
+{
+    this->outputFile = filePath;
+}
+
+void RobotMjcf::addCompiler(const std::string& angle, bool balanceIneratia)
+{
+    document->compiler().angle = angle;
+    document->compiler().balanceinertia = balanceIneratia;
+}
+
+void RobotMjcf::addDefaultsClass(float meshScale)
+{
+    mjcf::DefaultClass defaultsClass = document->default_().addClass(robot->getName());
+    
+    defaultsClass.insertComment("Add default values for " + robot->getName() + " here.", true);
+    
+    mjcf::Joint joint = defaultsClass.getElement<mjcf::Joint>();
+    joint.frictionloss = 1;
+    joint.damping = 0;
+    
+    mjcf::Mesh mesh = defaultsClass.getElement<mjcf::Mesh>();
+    mesh.scale = Eigen::Vector3f::Constant(meshScale);
+    
+    mjcf::Geom geom = defaultsClass.getElement<mjcf::Geom>();
+    geom.condim = 4;
+    
+    mjcf::ActuatorPosition actPos = defaultsClass.getElement<mjcf::ActuatorPosition>();
+    actPos.kp = 1;
+    
+    mjcf::ActuatorVelocity actVel = defaultsClass.getElement<mjcf::ActuatorVelocity >();
+    actVel.kv = 1;
+}
+
+void RobotMjcf::addSkybox(const Eigen::Vector3f& rgb1, const Eigen::Vector3f& rgb2)
+{
+    document->asset().addSkyboxTexture(rgb1, rgb2);
+}
+
+mjcf::Body RobotMjcf::addNodeBody(RobotNodePtr node, mjcf::Body parent,
+                                  bool addJoint, bool addInertial)
+{
+    mjcf::Body body = parent.addBody(node->getName());
+
+    if (node->hasParent())
+    {
+        Eigen::Matrix4f pose = node->getTransformationFrom(node->getParent());
+        math::Helpers::ScaleTranslation(pose, lengthScale);
+        body.setPose(pose);
+    }
+    
+    if (addJoint && (node->isRotationalJoint() || node->isTranslationalJoint()))
+    {
+        addNodeJoint(node, body);
+    }
+    
+    if (addInertial)
+    {
+        addNodeInertial(node, body);
+    }
+    
+    return body;
+}
+
+mjcf::Joint RobotMjcf::addNodeJoint(RobotNodePtr node, mjcf::Body body)
+{
+    VR_ASSERT(node->isRotationalJoint() xor node->isTranslationalJoint());
+    
+    mjcf::Joint joint = body.addJoint();
+    joint.name = node->getName();
+    
+    // get the axis
+    Eigen::Vector3f axis;
+    if (node->isRotationalJoint())
+    {
+        RobotNodeRevolutePtr revolute = boost::dynamic_pointer_cast<RobotNodeRevolute>(node);
+        VR_ASSERT(revolute);
+        axis = revolute->getJointRotationAxisInJointCoordSystem();
+    }
+    else if (node->isTranslationalJoint())
+    {
+        RobotNodePrismaticPtr prismatic = boost::dynamic_pointer_cast<RobotNodePrismatic>(node);
+        VR_ASSERT(prismatic);
+        axis = prismatic->getJointTranslationDirectionJointCoordSystem();
+    }
+    
+    joint.type = node->isRotationalJoint() ? "hinge" : "slide";
+    joint.axis = axis;
+    joint.limited = !node->isLimitless();
+    
+    if (!node->isLimitless())
+    {
+        Eigen::Vector2f range = { node->getJointLimitLow(), node->getJointLimitHigh() };
+        if (node->isTranslationalJoint())
+        {
+            range *= lengthScale;
+        }
+        
+        // Mujoco does not like ranges where min >= max.
+        if (std::abs(range(0) - range(1)) < 1e-6f)
+        {
+            range(1) = range(0) + 1e-6f;
+        }
+        joint.range = range;
+    }
+    
+    return joint;
+}
+
+mjcf::Inertial RobotMjcf::addNodeInertial(RobotNodePtr node, mjcf::Body body)
+{
+    const Eigen::Matrix3f matrix = node->getInertiaMatrix();
+    if (matrix.isIdentity(document->getFloatCompPrecision()) 
+        && node->getMass() < document->getFloatCompPrecision())
+    {
+        // Dont set an inertial element and let it be derived automatically.
+        return { };
+    }
+    
+    mjcf::Inertial inertial = body.addInertial();
+    inertial.pos = node->getCoMLocal() * lengthScale;
+    inertial.mass = node->getMass() * massScale;
+    inertial.inertiaFromMatrix(matrix);
+    
+    return inertial;
+}
+
+mjcf::Body RobotMjcf::addNodeBody(RobotNodePtr node, bool addJoint, bool addInertial)
+{
+    // See whether body for the node already exists.
+    auto find = nodeBodies.find(node->getName());
+    if (find != nodeBodies.end())
+    {
+        // Exists => break recursion.
+        return find->second;
+    }
+    
+    // Check whether body exists for parent node.
+    mjcf::Body parent;
+    
+    if (node->getName() == robot->getRootNode()->getName())
+    {
+        parent = robotBody;
+    }
+    else
+    {
+        find = nodeBodies.find(node->getParent()->getName());
+        if (find != nodeBodies.end())
+        {
+            // Parent exists.
+            parent = find->second;
+        }
+        else
+        {
+            // Parent does not exist => create it first.
+            parent = addNodeBody(robot->getRobotNode(node->getParent()->getName()),
+                                 addJoint, addInertial);
+        }
+    }
+    
+    // Add body as child of parent.
+    mjcf::Body body = addNodeBody(node, parent, addJoint, addInertial);
+    nodeBodies[node->getName()] = body;
+
+    return body;
+}
+
+
+void RobotMjcf::addNodeBodies()
+{
+    addNodeBodies(robot->getRobotNodeNames());
+}
+
+void RobotMjcf::addNodeBodies(const RobotNodeSet& nodeSet)
+{
+    addNodeBodies(nodeSet.getNodeNames());
+}
+
+void RobotMjcf::addNodeBodies(const std::vector<std::string>& nodeNames)
+{
+    for (const std::string& nodeName : nodeNames)
+    {
+        addNodeBody(robot->getRobotNode(nodeName));
+    }
+}
+
+
+}
diff --git a/VirtualRobot/XML/mujoco/RobotMjcf.h b/VirtualRobot/XML/mujoco/RobotMjcf.h
new file mode 100644
index 0000000000000000000000000000000000000000..b0f4eac18144054dd17605598f1af50c44f34be2
--- /dev/null
+++ b/VirtualRobot/XML/mujoco/RobotMjcf.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <filesystem>
+
+#include <VirtualRobot/Robot.h>
+#include <VirtualRobot/MJCF/Document.h>
+
+
+namespace VirtualRobot::mujoco
+{
+
+
+    class RobotMjcf
+    {
+    public:
+        
+        /// Construct with robot.
+        RobotMjcf(RobotPtr robot);
+        
+        
+        /// Get the document.
+        mjcf::Document& getDocument();
+        const mjcf::Document& getDocument() const;
+        
+        
+        // PREPARATION
+        
+        /// Get the robot.
+        RobotPtr getRobot() const;
+        
+        
+        
+        
+        /// Set the path to the output XML file.
+        void setOutputFile(const std::filesystem::path& filePath);
+        
+        
+        // MODIFIERS
+        
+        // META
+        
+        /// Make a compiler section.
+        void addCompiler(const std::string& angle = "radian", bool balanceIneratia = true);
+        
+        /// Make defaults class with robot's name.
+        void addDefaultsClass(float meshScale = 1.0f);
+        
+        
+        // ASSETS
+        
+        /// Add skybox texture.
+        void addSkybox(const Eigen::Vector3f& rgb1 = { .8f, .9f, .95f }, 
+                       const Eigen::Vector3f& rgb2 = { .4f, .6f, .80f });
+        
+        
+        // ROBOT NODES
+        
+        /// Add a body for the given robot node as child of `parent`.
+        mjcf::Body addNodeBody(RobotNodePtr node, mjcf::Body parent,
+                               bool addJoint = true, bool addInertial = true);
+        
+        /// Add a joint for the given node in `body` and return it.
+        mjcf::Joint addNodeJoint(RobotNodePtr node, mjcf::Body body);
+        /// Add an inertial for the given node in `body` and return it.
+        mjcf::Inertial addNodeInertial(RobotNodePtr node, mjcf::Body body);
+        
+        
+        /**
+         * @brief Add a body element for the given robot node.
+         * If its parent does not exist, create the parent body first.
+         * If `node` is the robot root node, it is attached to the robot body.
+         * @param addJoint Whether to add a joint, if applicable.
+         * @param addInertial Whether to add an inertial, if applicable.
+         * @return The node body.
+         */
+        mjcf::Body addNodeBody(RobotNodePtr node, bool addJoint = true, bool addInertial = true);
+
+        
+        /// Add bodies for all robot nodes.
+        void addNodeBodies();
+        /// Add bodies for the specified nodes.
+        void addNodeBodies(const RobotNodeSet& nodeSet);
+        /// Add bodies for the specified nodes.
+        void addNodeBodies(const std::vector<std::string>& nodeNames);
+        
+        
+    private:
+
+        /// The robot.
+        RobotPtr robot;
+        
+        /// The document. Using a pointer allows to easily reset it.
+        mjcf::DocumentPtr document { new mjcf::Document() };
+        
+        /// The path to the output XML file.
+        std::filesystem::path outputFile;
+        
+        
+        /// Scaling for lengths, such as positions and translations (to m).
+        float lengthScale = 1.f;
+        /// Scaling for lengths, such as positions and translations (to m).
+        float meshScale = 1.f;
+        /// Scaling for mass (to kg).
+        float massScale = 1.f;
+        
+        
+        /// The robot body. (Not part of the original robot structure.)
+        mjcf::Body robotBody;
+        
+        /// Map of robot node names to XML elements.
+        std::map<std::string, mjcf::Body> nodeBodies;
+        
+    };
+
+}