diff --git a/.claude/skills/mujoco-cpp-build/SKILL.md b/.claude/skills/mujoco-cpp-build/SKILL.md new file mode 100644 index 0000000..8f499ed --- /dev/null +++ b/.claude/skills/mujoco-cpp-build/SKILL.md @@ -0,0 +1,16 @@ +--- +name: mujoco-cpp-build +description: Use when building C++ examples, compiling simulation executables, configuring CMake, or resolving C++ building errors in the MuJoCo learning repository. It requires the agent to actively prompt the user for build requirements (source vs release path, target directories). +--- + +# MuJoCo C++ Build + +This is the Claude Code project-local adapter for the canonical skill at: + +`../../../skills/mujoco-cpp-build/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions: + +- `../../../skills/mujoco-cpp-build/SKILL.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.claude/skills/mujoco-engineering/SKILL.md b/.claude/skills/mujoco-engineering/SKILL.md new file mode 100644 index 0000000..ee848f4 --- /dev/null +++ b/.claude/skills/mujoco-engineering/SKILL.md @@ -0,0 +1,17 @@ +--- +name: mujoco-engineering +description: Use when an agent needs to implement, reproduce, debug, or extend MuJoCo projects using this repository's MJCF, Python, C++, ray casting, soft contact, sensors, rendering, viewer, force, or build examples. +--- + +# MuJoCo Engineering + +This is the Claude Code project-local adapter for the canonical skill at: + +`../../../skills/mujoco-engineering/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions and its reference map: + +- `../../../skills/mujoco-engineering/SKILL.md` +- `../../../skills/mujoco-engineering/references/tutorial-map.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.claude/skills/mujoco-teaching/SKILL.md b/.claude/skills/mujoco-teaching/SKILL.md new file mode 100644 index 0000000..1ae3b1e --- /dev/null +++ b/.claude/skills/mujoco-teaching/SKILL.md @@ -0,0 +1,17 @@ +--- +name: mujoco-teaching +description: Use when a user wants to learn MuJoCo concepts, MJCF modeling, Python/C++ MuJoCo APIs, sensors, rendering, contact, soft contact, ray casting, or asks conceptual/tutorial questions about this repository's MuJoCo lessons. +--- + +# MuJoCo Teaching + +This is the Claude Code project-local adapter for the canonical skill at: + +`../../../skills/mujoco-teaching/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions and its reference map: + +- `../../../skills/mujoco-teaching/SKILL.md` +- `../../../skills/mujoco-teaching/references/tutorial-map.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.gitignore b/.gitignore index 3ec7f79..c22a762 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,21 @@ extend/equality/.cache/ extend/plugin/.cache/ +__pycache__/ +*.py[cod] +.venv/ +*.egg-info/ +build/ +dist/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.uv/ +uv.lock +skills/*/.local/ + extend/soft_contact/C++/build/ extend/soft_contact/C++/.cache/ extend/touch/C++/build/ -extend/touch/C++/.cache/ \ No newline at end of file +extend/touch/C++/.cache/ diff --git a/.opencode/skills/mujoco-cpp-build/SKILL.md b/.opencode/skills/mujoco-cpp-build/SKILL.md new file mode 100644 index 0000000..700aa06 --- /dev/null +++ b/.opencode/skills/mujoco-cpp-build/SKILL.md @@ -0,0 +1,18 @@ +--- +name: mujoco-cpp-build +description: Use when building C++ examples, compiling simulation executables, configuring CMake, or resolving C++ building errors in the MuJoCo learning repository. It requires the agent to actively prompt the user for build requirements (source vs release path, target directories). +metadata: + canonical-source: skills/mujoco-cpp-build +--- + +# MuJoCo C++ Build + +This is the OpenCode project-local adapter for the canonical skill at: + +`../../../skills/mujoco-cpp-build/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions: + +- `../../../skills/mujoco-cpp-build/SKILL.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.opencode/skills/mujoco-engineering/SKILL.md b/.opencode/skills/mujoco-engineering/SKILL.md new file mode 100644 index 0000000..d5326c6 --- /dev/null +++ b/.opencode/skills/mujoco-engineering/SKILL.md @@ -0,0 +1,19 @@ +--- +name: mujoco-engineering +description: Use when an agent needs to implement, reproduce, debug, or extend MuJoCo projects using this repository's MJCF, Python, C++, ray casting, soft contact, sensors, rendering, viewer, force, or build examples. +metadata: + canonical-source: skills/mujoco-engineering +--- + +# MuJoCo Engineering + +This is the OpenCode project-local adapter for the canonical skill at: + +`../../../skills/mujoco-engineering/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions and its reference map: + +- `../../../skills/mujoco-engineering/SKILL.md` +- `../../../skills/mujoco-engineering/references/tutorial-map.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.opencode/skills/mujoco-teaching/SKILL.md b/.opencode/skills/mujoco-teaching/SKILL.md new file mode 100644 index 0000000..bad336b --- /dev/null +++ b/.opencode/skills/mujoco-teaching/SKILL.md @@ -0,0 +1,19 @@ +--- +name: mujoco-teaching +description: Use when a user wants to learn MuJoCo concepts, MJCF modeling, Python/C++ MuJoCo APIs, sensors, rendering, contact, soft contact, ray casting, or asks conceptual/tutorial questions about this repository's MuJoCo lessons. +metadata: + canonical-source: skills/mujoco-teaching +--- + +# MuJoCo Teaching + +This is the OpenCode project-local adapter for the canonical skill at: + +`../../../skills/mujoco-teaching/SKILL.md` + +When this skill is loaded, read and follow the canonical skill instructions and its reference map: + +- `../../../skills/mujoco-teaching/SKILL.md` +- `../../../skills/mujoco-teaching/references/tutorial-map.md` + +Use repository-relative tutorial paths only. Do not store user-specific paths in this adapter. diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..8982557 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,24 @@ +{ + "configurations": [ + { + "browse": { + "databaseFilename": "${workspaceFolder}/.vscode/browse.vc.db", + "limitSymbolsToIncludedHeaders": false + }, + "includePath": [ + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_hg/include/**", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_go/include/**", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_api/include/**", + "/home/albusgive2/rlfw/rlfw_interface/install/rlfw_msgs/include/**", + "/opt/ros/humble/include/**", + "/usr/include/**" + ], + "name": "ros2", + "intelliSenseMode": "gcc-x64", + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu11", + "cppStandard": "c++17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index abc5eaa..28470a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,21 @@ "cmake.sourceDirectory": "/home/albusgive2/mujoco_learning/CPP/Chapter1-make", "python-envs.defaultEnvManager": "ms-python.python:conda", "python-envs.defaultPackageManager": "ms-python.python:conda", - "python-envs.pythonProjects": [] + "ROS2.distro": "humble", + "python.autoComplete.extraPaths": [ + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_hg/local/lib/python3.10/dist-packages", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_go/local/lib/python3.10/dist-packages", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_api/local/lib/python3.10/dist-packages", + "/home/albusgive2/rlfw/rlfw_interface/install/rlfw_msgs/local/lib/python3.10/dist-packages", + "/opt/ros/humble/lib/python3.10/site-packages", + "/opt/ros/humble/local/lib/python3.10/dist-packages" + ], + "python.analysis.extraPaths": [ + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_hg/local/lib/python3.10/dist-packages", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_go/local/lib/python3.10/dist-packages", + "/home/albusgive2/unitree_ros2/cyclonedds_ws/install/unitree_api/local/lib/python3.10/dist-packages", + "/home/albusgive2/rlfw/rlfw_interface/install/rlfw_msgs/local/lib/python3.10/dist-packages", + "/opt/ros/humble/lib/python3.10/site-packages", + "/opt/ros/humble/local/lib/python3.10/dist-packages" + ] } \ No newline at end of file diff --git a/CPP/Chapter3-get_obj/tutorial.md b/CPP/Chapter3-get_obj/tutorial.md index c1fa30a..df0d974 100644 --- a/CPP/Chapter3-get_obj/tutorial.md +++ b/CPP/Chapter3-get_obj/tutorial.md @@ -1,23 +1,107 @@ -# get obj -## 获取名字 -数量 - -**先看mjModel结构体开头部分,这里的命名方式都是nXXX,这代表各个元素的数量** - -## 获取id -`MJAPI int mj_name2id(const mjModel* m, int type, const char* name);` -**通过name获取实体的id -m :mjModel -type: mjmodel.h文件中的mjtObj中定义,这个是要获取id的实体类型一下是部分type类型枚举,在mjtObj中找到 -name: name -** - -## 获取位置 - -**可以通过 xpos和xxx_xpos获取各个对象的位置** -## 获取姿态 -**通过xquat可以获取body的姿态** +# 获取仿真世界中的实体信息 (Get Object Info) +在 MuJoCo 中,获取仿真世界中各种实体(Body, Joint, Geom 等)的数量、ID、位置及姿态是非常高频的操作。以下是实现逻辑与核心数据结构。 +--- +## 1. 获取实体数量与名称 (Get Counts & Names) +所有的数量与命名信息都存储在 `mjModel` 结构体中。 +### 1.1 实体数量定义 +在 `mjModel` 的开头,定义了所有仿真元素的数量(以 `n` 开头): +```cpp +// 摘自 mujoco/mjmodel.h: 实体数量声明 +int nbody; // 身体 (Body) 数量 +int njnt; // 关节 (Joint) 数量 +int ngeom; // 几何体 (Geom) 数量 +int nsite; // 标记点 (Site) 数量 +int ncam; // 相机 (Camera) 数量 +int nlight; // 光源 (Light) 数量 +int nmesh; // 网格 (Mesh) 数量 +int nsensor; // 传感器 (Sensor) 数量 +int nactuator; // 驱动器 (Actuator) 数量 +``` +* 源码链接:[mujoco/mjmodel.h (sizes段)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +### 1.2 实体名字缓冲区 +在 `mjModel` 中,名字均以一维字符缓冲区存储,并通过对应的地址数组记录每个实体的名字指针偏置: +```cpp +// 摘自 mujoco/mjmodel.h: 命名缓冲区 +char* names; // 名字缓存区,字符大数组 (nnames x 1) +int* name_bodyadr; // 各 Body 名字在 names 中的起始索引 (nbody x 1) +int* name_jntadr; // 各 Joint 名字在 names 中的起始索引 (njnt x 1) +int* name_geomadr; // 各 Geom 名字在 names 中的起始索引 (ngeom x 1) +int* name_sensoradr; // 各 Sensor 名字在 names 中的起始索引 (nsensor x 1) +``` +* 源码链接:[mujoco/mjmodel.h (names段)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +--- + +## 2. 获取实体 ID (Get ID by Name) +因为 MuJoCo 底层物理计算全部基于数组索引(即 ID),在 API 中我们通常需要通过名字(string)获取其 ID。 +使用 API 函数: +```cpp +MJAPI int mj_name2id(const mjModel* m, int type, const char* name); +``` + +### 2.1 参数说明 +* `m`: `mjModel` 指针。 +* `type`: 实体的类型,由枚举类型 `mjtObj` 定义。 +* `name`: 实体的名称。 + +### 2.2 实体类型枚举 `mjtObj` +`mjtObj` 定义在 `mjmodel.h` 中,代表各种可能的实体类型: +```cpp +typedef enum mjtObj_ { + mjOBJ_UNKNOWN = 0, // 未知对象类型 + mjOBJ_BODY, // 身体 (Body) + mjOBJ_XBODY, // Body的替代表示,用于读取常规参考系 (而非惯性系) + mjOBJ_JOINT, // 关节 (Joint) + mjOBJ_DOF, // 自由度 (Degrees of freedom) + mjOBJ_GEOM, // 几何体 (Geom) + mjOBJ_SITE, // 标记点 (Site) + mjOBJ_CAMERA, // 相机 (Camera) + mjOBJ_LIGHT, // 光源 (Light) + mjOBJ_FLEX, // 柔性可变形体 (Flex) + mjOBJ_MESH, // 三角网格 (Mesh) + mjOBJ_SKIN, // 皮肤网格 (Skin) + mjOBJ_HFIELD, // 高度图 (Heightfield) + mjOBJ_TEXTURE, // 纹理 (Texture) + mjOBJ_MATERIAL, // 材质 (Material) + mjOBJ_PAIR, // 特殊碰撞 geom 对 (Pair) + mjOBJ_SENSOR // 传感器 (Sensor) +} mjtObj; +``` +* 源码链接:[mujoco/mjmodel.h (mjtObj定义)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +演示代码: +```cpp +int body_id = mj_name2id(model, mjOBJ_BODY, "robot_link1"); +``` + +--- + +## 3. 获取位置与姿态 (Get Pos & Ori) +物体的实时笛卡尔空间状态(三维位置、姿态矩阵等)全部存储在 **`mjData`** 中,由求解器在 `mj_step` 时进行实时更新更新: + +```cpp +// 摘自 mujoco/mjdata.h: 笛卡尔坐标状态 +mjtNum* xpos; // 各个 Body 的三维笛卡尔中心坐标 (nbody x 3) +mjtNum* xquat; // 各个 Body 的笛卡尔四元数旋转姿态 (nbody x 4) +mjtNum* xmat; // 各个 Body 的笛卡尔旋转矩阵姿态 (nbody x 9) +mjtNum* xipos; // 各个 Body 质心 (com) 的笛卡尔坐标 (nbody x 3) +mjtNum* ximat; // 各个 Body 质心的笛卡尔旋转矩阵 (nbody x 9) +``` +* 源码链接:[mujoco/mjdata.h (Cartesian state)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjdata.h) + +演示代码: +```cpp +// 获取特定 body 实时三维位置坐标 +int body_id = mj_name2id(model, mjOBJ_BODY, "support"); +if (body_id != -1) { + double x = data->xpos[body_id * 3 + 0]; + double y = data->xpos[body_id * 3 + 1]; + double z = data->xpos[body_id * 3 + 2]; + std::cout << "Body position: " << x << ", " << y << ", " << z << std::endl; +} +``` diff --git a/CPP/Chapter4-sensor_data/tutorial.md b/CPP/Chapter4-sensor_data/tutorial.md index 031a499..e86a6a3 100644 --- a/CPP/Chapter4-sensor_data/tutorial.md +++ b/CPP/Chapter4-sensor_data/tutorial.md @@ -1,9 +1,15 @@ # 传感器数据获取 - -**sensordata的索引需要依靠mjData的sensor_adr获取,这个可以使用sensor的id** -**这个我们要注意传感器具有的数据量,有的传感器是一个值,而有的传感器是三个值。我们可以使用mjModel中的sensor_dim获得传感器输出的参数量** -*演示:* +**`sensordata` 的索引需要依靠 `mjModel` 中的 `sensor_adr` 获取,也可以使用 sensor 的 id。** + +**我们要注意传感器具有的数据量,有的传感器是一个值,而有的传感器是三个值。我们可以使用 `mjModel` 中的 `sensor_dim` 获得传感器输出的参数量。** + +* `mjModel.sensor_adr` 的定义类似于: +```cpp +int* sensor_adr; // address in sensor array (nsensor x 1) +``` + +演示: ```C++ std::vector get_sensor_data(const mjModel *model, const mjData *data, @@ -21,8 +27,24 @@ std::vector get_sensor_data(const mjModel *model, const mjData *data, return sensor_data; } ``` -对于传感器其他的属性在 mjModel中可以直接获得。如下: - + +对于传感器的其他属性,在 `mjModel` 中可以直接获得: +```cpp +// mjModel 中的传感器相关成员变量说明 +int* sensor_type; // 传感器类型 (mjtSensor) (nsensor x 1) +int* sensor_datatype; // 数值数据类型 (mjtDataType) (nsensor x 1) +int* sensor_needstage; // 所需 Jun 算阶段 (mjtStage) (nsensor x 1) +int* sensor_objtype; // 被测物体的类型 (mjtObj) (nsensor x 1) +int* sensor_objid; // 被测物体的 ID (nsensor x 1) +int* sensor_reftype; // 参考系物体的类型 (mjtObj) (nsensor x 1) +int* sensor_refid; // 参考系物体的 ID; -1: 全局参考系 (nsensor x 1) +int* sensor_dim; // 标量输出的数量 (nsensor x 1) +int* sensor_adr; // 传感器数据在 sensordata 中的地址 (nsensor x 1) +mjtNum* sensor_cutoff; // 实数/正数的截止值; 0: 忽略 (nsensor x 1) +mjtNum* sensor_noise; // 噪声音频标准差 (nsensor x 1) +mjtNum* sensor_user; // 用户数据 (nsensor x nuser_sensor) +int* sensor_plugin; // 插件实例 ID; -1: 非插件 (nsensor x 1) +``` ### 读取相机画面 相机来源一般是在模型文件中创建相机,或者创建一个相机手动控制,就像 base中 diff --git a/CPP/Chapter5-draw/tutorial.md b/CPP/Chapter5-draw/tutorial.md index 0c3f2eb..a2d1e66 100644 --- a/CPP/Chapter5-draw/tutorial.md +++ b/CPP/Chapter5-draw/tutorial.md @@ -1,13 +1,45 @@ # 3D绘制 mujoco提供显示基础几何体和mujoco提供的一些特殊渲染几何体。查看文档可知mjv_initGeom函数能在渲染场景中增加几何体,mjv_connector可以用mjv_initGeom初始化的几何体绘制提供的一些特殊形状(如箭头,直线等)。mujoco显示画面的原理是通过mjv_updateScene 将仿真数据储存到mjvScene中,这是已经处理好的几何数据,接下来使用mjr_render传递给opengl渲染。我们在绘制过程中是要在仿真的几何数据处理完之后,加入绘制信息,再交给opengl渲染。 在mjvScene中添加信息,其实是直接在mjvScene的geoms后面续写,而且要增加ngeom长度。这里通过注释可以理解,mjvScene根据ngeom确定几何体数量再从geoms中获取资源。 - + +```c +struct _mjvScene { + // abstract geoms + int maxgeom; // size of allocated geom buffer + int ngeom; // number of geoms currently in buffer + mjvGeom* geoms; // buffer for geoms + int* geomorder; // buffer for ordering geoms by distance to camera + // ... +}; +typedef struct _mjvScene mjvScene; +``` + mjv_initGeom函数原型: - + +```c +void mjv_initGeom(mjvGeom* geom, int type, const mjtNum size[3], + const mjtNum pos[3], const mjtNum mat[9], const float rgba[4]); +``` + mjv_connector函数原型: - + +```c +void mjv_connector(mjvGeom* geom, int type, mjtNum width, + const mjtNum from[3], const mjtNum to[3]); +``` + geom是传入的仅绘制的几何体,需要使用mjv_initGeom初始化,type见下面,width是绘制的宽度,这个是对于渲染出来的画面的宽度,from起点,to终点 - + +```c +typedef enum _mjtGeom { + // ... + mjGEOM_ARROW = 100, // 箭头 + mjGEOM_ARROW1, // 无楔形箭头 + mjGEOM_ARROW2, // 双向箭头 + mjGEOM_LINE, // 直线 + // ... +} mjtGeom; +``` 这里是可以绘制的几何形状类型,分别是箭头,无楔形箭头,双向箭头,直线。 *演示——绘制几何体函数:* @@ -71,9 +103,20 @@ mjr_render(viewport, &scn, &con); # 2D绘制 字体尺寸的初始化: - -查阅文档我们可知2D绘制要在mjr_render之后进行 - + +```c +typedef enum _mjtFontScale { + mjFONTSCALE_50 = 50, // 50% 缩放,适用于低分辨率渲染 + mjFONTSCALE_100 = 100, // 100% 缩放,默认普通比例 + mjFONTSCALE_150 = 150, // 150% 缩放 + mjFONTSCALE_200 = 200 // 200% 缩放 +} mjtFontScale; +``` + +查阅文档我们可知,2D 绘制函数需要在 `mjr_render` 之后进行调用: + +> [!IMPORTANT] +> `mjr_render` 会在开始渲染时清除视口(Viewport)。因此,所有自定义的 2D 绘制操作(如绘制文本、矩形、表格或标签等)必须在调用 `mjr_render` **之后** 进行,否则绘制的内容会被视口清除覆盖。 *绘制文本:* diff --git a/CPP/Chapter6-force/tutorial.md b/CPP/Chapter6-force/tutorial.md index b7bfd3e..6958f0d 100644 --- a/CPP/Chapter6-force/tutorial.md +++ b/CPP/Chapter6-force/tutorial.md @@ -1,22 +1,47 @@ # 作用力 **平动** + $$ m \cdot a = F $$ + **旋转** + $$ I \cdot \alpha = \tau $$ + $$ F/\tau = 外部力+驱动力+被动力+约束力+偏置力 $$ # 外部力 我们查看文档计算部分可以看到有qfrc_passive,qfrc_actuator,qfrc_applied三个力分别对应被动力,驱动力,外部力 - +```c +mjtNum* qfrc_applied; // applied generalized force (nv x 1) +mjtNum* qfrc_passive; // passive generalized force (nv x 1) +mjtNum* qfrc_actuator; // actuator generalized force (nv x 1) +``` + 只要看手册的api或者头文件中,找到mj_applyFT函数应用外部力。 - + +```c +void mj_applyFT(const mjModel* m, mjData* d, + const mjtNum force[3], const mjtNum torque[3], + const mjtNum point[3], int body, + mjtNum* qfrc_target); +``` + 或者还可以使用xfrc_applied直接作用外部力在质心上。 - + +```c +mjtNum* xfrc_applied; // applied Cartesian forces (nbody x 6) +``` + **这里也说明了是笛卡尔力。** mj_applyFT函数的参数,是三维的力,三维扭矩,三维坐标(worldbody坐标系),bodyid。qfrc_target可以直接使用d->qfrc_applied。mj_applyFT是对于body在 **“自由度”** 上施加力。于是我们可以使用两个方式对body施加外部力。 qfrc_target还可以是以下这些被动力等qfrc_xxx的力 - + +```c +mjtNum* qfrc_passive; // passive generalized force (nv x 1) +mjtNum* qfrc_bias; // constraint bias generalized force (nv x 1) +mjtNum* qfrc_constraint; // constraint generalized force (nv x 1) +``` **mj_Data接口演示(作用在质心上):** ```C++ @@ -35,29 +60,65 @@ mj_applyFT(m, d, force, torque, point, id, d->qfrc_applied); **mj_applyFT每次调用都是增量式,如果我们想清除力可以使用mju_zero,如mju_zero(d->qfrc_applied, m->nv);** # 驱动力 - +```c +mjtNum* qfrc_actuator; // actuator generalized force (nv x 1) +``` mjData.qfrc_actuator是驱动器执行的力,不同驱动器最终会计算出力或者扭矩作用到关节上。 # 被动力 mjData.qfrc_passive是被动力,关节参数的damping,stiffness,摩擦力,流体阻力都会最终计算到改力中。 + $$ damping_force = (0-qvel)*damping $$ + $$ stiffness_force = (0-qpos)*stiffness $$ # 约束力 +```c +mjtNum* efc_force; // constraint force (nefc x 1) +``` mjData.efc_force是约束力,关节的frictionloss,equality计算出来的合力为改力。 - jar = Jac*qacc-aref 残差=雅可比*关节加速度-参考伪加速度 + 这里 $jar_0 = Jac \cdot qacc_0 - aref$ 表示无约束时的约束空间残差(未施加摩擦力时的相对加速度偏离量),$InverseConstraintMass$ 是该约束维度的质量倒数(即惯量响应矩阵 $A = J M^{-1} J^T + R$ 的对应对角元素)。 + +关节干摩擦力(静摩擦与滑动摩擦)的完整解算公式为: $$ -frictionlossforce = +frictionloss\_force = \begin{cases} - frictionloss, & \text{if } jar <= -InverseConstraintMass ⋅ floss \\ - -frictionloss, & \text{else if } jar>=InverseConstraintMass ⋅ floss \\ - (没看懂), & \text{else } + frictionloss, & \text{if } jar_0 \le -InverseConstraintMass \cdot floss & \text{(正向滑动摩擦)} \\ + -frictionloss, & \text{else if } jar_0 \ge InverseConstraintMass \cdot floss & \text{(反向滑动摩擦)} \\ + -\frac{jar_0}{InverseConstraintMass}, & \text{else } & \text{(静摩擦/粘滞状态,此时实际残差 } jar = 0\text{)} \end{cases} $$ + +**原理解析**: +1. **滑动摩擦阶段**:当外力推动关节的趋势(即 $jar_0$)超过了最大静摩擦力所能提供的阻碍加速度时,摩擦力饱和,大小恒为最大摩擦力 $floss$(即 `frictionloss`),方向与相对运动趋势相反。 +2. **静摩擦(粘滞)阶段**:当外力较小,在摩擦力能平衡的范围内(即 `else` 分支),摩擦力会自动产生一个恰好抵消相对运动趋势的力 $-\frac{jar_0}{InverseConstraintMass}$,使得最终的实际加速度残差 $jar$ 完美归零,即物体保持静止或相对匀速运动。 **源码实现(SRC/engine/engine_core_constraint.c)** - +```c +// linear friction limits +else if (floss && floss[i] > 0) { + // linear negative friction (正向滑动摩擦) + if (jar[i] <= -R[i] * floss[i]) { + force[i] = floss[i]; + state[i] = mjCNSTRSTATE_LINEARNEG; + } + + // linear positive friction (反向滑动摩擦) + else if (jar[i] >= R[i] * floss[i]) { + force[i] = -floss[i]; + state[i] = mjCNSTRSTATE_LINEARPOS; + } + + // quadratic/inactive friction (静摩擦/粘滞状态) + else { + force[i] = -jar[i] / R[i]; + state[i] = mjCNSTRSTATE_QUADRATIC; + } +} +``` # 偏置力 - +```c +mjtNum* qfrc_bias; // constraint bias generalized force (nv x 1) +``` mjData.qfrc_bias科里奥利力等,由引擎自动计算。 \ No newline at end of file diff --git a/CPP/Chapter8-ray/tutorial.md b/CPP/Chapter8-ray/tutorial.md index 0c8e218..2a2fd6f 100644 --- a/CPP/Chapter8-ray/tutorial.md +++ b/CPP/Chapter8-ray/tutorial.md @@ -1,18 +1,53 @@ -# Ray -射线测距接口 - +# 射线检测 (Ray Casting) + +射线检测(Ray Casting)常用于模拟激光雷达、超声波测距传感器,或者进行视线、碰撞预判。在仿真运行到 `mj_kinematics` 或 `mj_fwdPosition` 之后方可调用(因为射线检测依赖实体的实时位置信息)。 + +--- + +## 1. 核心 API 说明 + +### 1.1 多束射线检测 `mj_multiRay` +在一处起点发射多条不同方向的射线,常用于激光雷达雷达阵列: +```cpp void mj_multiRay(const mjModel* m, mjData* d, const mjtNum pnt[3], const mjtNum* vec, const mjtByte* geomgroup, mjtByte flg_static, int bodyexclude, int* geomid, mjtNum* dist, int nray, mjtNum cutoff); -* m: mjModel -* d: mjData -* pnt: 射线起点 -* vec: 射线向量 -* geomgroup:检测分组,NULL代表检测所有 -* flg_static: 是否检测静态物体 1检测 0不检测 -* bodyexclude: -1 可用于指示包括所有主体 -* geomid: 检测到的几何体id,没有为-1 -* dist: 到geom表面的距离,数据是距离比例,是vec的倍数,无限远为-1 -* nray:射线数量 -* cutoff: 距离截断,超过截断距离的射线不检测,判断条件(pnt到geompos中心>cutoff+几何体外接球半径) -**其余ratXXX参数同理** \ No newline at end of file +``` + +### 1.2 单条射线检测 `mj_ray` +检测单个射线的交点: +```cpp +mjtNum mj_ray(const mjModel* m, const mjData* d, const mjtNum pnt[3], const mjtNum vec[3], + const mjtByte* geomgroup, mjtByte flg_static, int bodyexclude, + int geomid[1]); +``` + +### 1.3 其他特定几何类型检测 +* `mj_rayHfield`: 与高度图进行求交检测。 +* `mj_rayMesh`: 与三角网格模型(Mesh)求交检测。 +* `mju_rayGeom`: 用于纯几何计算(不受当前世界状态约束,计算指定位置尺寸的 geom)。 + +--- + +## 2. 关键参数详解 + +* **`pnt`** (`const mjtNum[3]`): 射线发射起点的 3D 笛卡尔坐标。 +* **`vec`** (`const mjtNum*`): 射线方向向量。 + * 对于 `mj_multiRay`,其为长度为 `nray * 3` 的数组,每 3 个元素代表一条射线的方向向量。 +* **`geomgroup`** (`const mjtByte*`): 几何体分组使能数组(长度为 `mjNGROUP`,通常为 5)。 + * 设为 `NULL` 代表检测所有 Geom 分组。若不为 `NULL`,则只有对应位为 1 的 Geom 组才会被检测。 +* **`flg_static`** (`mjtByte`): 是否检测静态(Static)物体。`1` 代表检测静态物体,`0` 代表忽略静态物体。 +* **`bodyexclude`** (`int`): 需要排除检测的 Body ID。 + * 设为 `-1` 代表检测所有 Body。 +* **`geomid`** (`int*`): 传出参数。检测到相交的 Geom ID。若未击中任何物体,则返回 `-1`。 +* **`dist`** (`mjtNum*`): 传出参数。到 Geom 表面的距离比例值。 + * **重要**:返回值不是绝对距离(单位米),而是**方向向量 `vec` 的缩放倍数**。即实际碰撞点坐标为: + $$pos_{hit} = pnt + dist \times vec$$ + * 若未发生碰撞,返回值为 `-1`。 +* **`nray`** (`int`): `mj_multiRay` 批量发射的射线总数。 +* **`cutoff`** (`mjtNum`): 距离截断阈值。如果起点到几何体中心点的距离大于 `cutoff` 加上该几何体的包围球半径,则直接跳过检测以提升性能。 + +--- + +## 3. 官方文档入口 +* [MuJoCo 官方 API 射线检测 (Ray Collisions)](https://mujoco.readthedocs.io/en/latest/programming.html#ray-collisions) \ No newline at end of file diff --git a/MJCF/Chapter2-virtual_world/tutorial.md b/MJCF/Chapter2-virtual_world/tutorial.md index cdf0683..2d6efeb 100644 --- a/MJCF/Chapter2-virtual_world/tutorial.md +++ b/MJCF/Chapter2-virtual_world/tutorial.md @@ -21,13 +21,29 @@ density="1.225" viscosity="1.8e-5"/> * magnetic="0 -0.5 0"世界磁场,影响磁力传感器 * density 介质密度,水,空气等,单位kg/m³ * viscosity 介质粘度 -* integrator= [Euler/RK4/implicit/implicitfast]积分器,默认欧拉,用于仿真世界每步就求解计算,各个优势见下图 -* solver= [PGS, CG, Newton]求解器配置,默认牛顿 -* iterations="100" 约束求解器最大迭代次数,按需配置。 -求解器其他配置: - -积分器比较: - +* **integrator** (`[Euler/RK4/implicit/implicitfast]`): 积分器,默认欧拉(Euler),用于每个仿真步长的物理求解。各自特点如下: + * `Euler`: 简单快速,但精度低,适用于快速测试或简单系统。 + * `RK4`: 精度高,适用于对精度有要求但计算量不敏感的场景。 + * `Implicit`: 隐式积分,适合刚性系统(Stiff systems)和稳定性要求高的场景,但计算复杂度较高。 + * `ImplicitFast`: 在保证稳定性的前提下加快计算速度,适用于大规模复杂仿真。 +* **solver** (`[PGS, CG, Newton]`): 约束求解器类型,默认牛顿法(Newton)。 +* **iterations** (`"100"`): 约束求解器的最大迭代次数,按需配置。 + +#### 求解器细分配置参数 (Solver Attributes) +当需要对求解精度、性能进行更微观的调优时,可以在 `` 中配置以下选项(均有其对应的官方默认值,大部分情况下无需手动配置): + +| 选项名称 | 类型与默认值 | 详细功能说明 | 官方文档参考 | +| :--- | :--- | :--- | :--- | +| `iterations` | `int, "100"` | 约束求解器的最大迭代次数。暖启动(Warmstart)开启时,能用较少迭代得到高精度解。 | [option-iterations](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-iterations) | +| `tolerance` | `real, "1e-8"` | 迭代求解器提前终止的容差阈值。当两次迭代的改进/梯度范数小于此值时自动停止。若设为 0 则禁用提前终止。 | [option-tolerance](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-tolerance) | +| `ls_iterations`| `int, "50"` | CG / Newton 求解器执行线搜索(Line Search)的最大迭代次数。 | [option-ls_iterations](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-ls_iterations) | +| `ls_tolerance` | `real, "0.01"` | 提前终止线搜索算法的容差阈值。 | [option-ls_tolerance](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-ls_tolerance) | +| `noslip_iterations`| `int, "0"` | Noslip 求解器的最大迭代次数(后处理步骤)。用于抑制软约束模型在摩擦维度上产生的多余滑移与漂移。设为 0 即禁用。 | [option-noslip_iterations](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-noslip_iterations) | +| `noslip_tolerance` | `real, "1e-6"` | 提前终止 Noslip 求解器的容差阈值。 | [option-noslip_tolerance](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-noslip_tolerance) | +| `ccd_iterations` | `int, "50"` | 凸面碰撞检测(CCD)算法的最大迭代次数。一般无需调整,除非几何体具有极大长宽比。 | [option-ccd_iterations](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-ccd_iterations) | +| `ccd_tolerance` | `real, "1e-6"` | 凸面碰撞算法提前终止的容差阈值。 | [option-ccd_tolerance](https://mujoco.readthedocs.io/en/latest/option-ccd_tolerance) | +| `sdf_iterations` | `int, "10"` | 有向距离场(SDF)碰撞的迭代次数(每个初始点)。 | [option-sdf_iterations](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-sdf_iterations) | +| `sdf_initpoints` | `int, "40"` | 寻找 Signed Distance Field 碰撞接触点时的起点数。 | [option-sdf_initpoints](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-sdf_initpoints) | ## 可视化配置 ### visual节点 diff --git a/MJCF/Chapter4-joint/tutorial.md b/MJCF/Chapter4-joint/tutorial.md index 7839650..773d77b 100644 --- a/MJCF/Chapter4-joint/tutorial.md +++ b/MJCF/Chapter4-joint/tutorial.md @@ -1,16 +1,95 @@ -# joint - - joint将body之间连接在一起,使其可以进行活动。这么说吧,body中的所有geom为一个整体然后joint是连接这些整体的。就是body和body靠joint活动,body中只能有一个joint用来连接当前body和上一层body。再根本一点就是joint对于上一层body是相对静止的,当前body与joint是在运动。 -* name -* tpye="[free/ball/slide/hinge]" 自由关节,一般不用;球形关节,绕球旋转;滑轨;旋转关节 -* pos="0 0 0"关节在body的位置 -* axis="0 0 1" x,y,z活动轴,只有slide和hinge有用 -* stiffness="0" 弹簧,数值正让关节具有弹性 -**`(0-pos)*stiffness`** -* range="0 0" 关节限制,当球形时只有二参有效,一参设置为0,但是要在compiler指定autolimits -* limited="auto" 此属性指定关节是否有限制 -* damping="0" 阻尼 交叉滚子轴承damping -**`(0-v)*damping`** -* frictionloss="0"关节摩擦损失 -* armature="0" 电枢 转子转动惯量*减速比^2(很小的值) -* ref 角度偏置 \ No newline at end of file +# 关节 (Joint) +*** 关节(Joint)用于连接相邻的 Body,并定义它们之间的相对自由度(DoF)*** + +在 MuJoCo 中,所有的几何体(Geom)都必须依附于身体(Body)。关节的作用是声明当前 Body 相对于其父级 Body 的运动方式。 +* **运动主体**:关节对于其父级 Body 在物理上是相对静止的,当前 Body 则围绕/沿着关节定义的方向进行运动。 +* **约束限制**:当前 Body 内部最多只能定义一组关节,用来表达它与上一层父级 Body 之间的连接关系。 + +--- + +## 1. 关节类型 (`type`) + +MuJoCo 支持四种核心关节类型: + +| 关节类型 | 英文名称 | 自由度 (DoF) | 运动描述 | 适用场景 | +| :--- | :--- | :---: | :--- | :--- | +| **旋转关节** | `hinge` | 1 | 围绕指定的 `axis` 进行单轴旋转。 | 摆臂、轮轴、人体关节。 | +| **滑动关节** | `slide` | 1 | 沿着指定的 `axis` 进行单轴平移。 | 电梯、活塞、伸缩机构。 | +| **球关节** | `ball` | 3 | 绕关节中心进行任意三维旋转(类似于球头万向节)。 | 肩关节、摇杆。 | +| **自由关节** | `free` | 6 | 包含 3 维平移与 3 维旋转,允许物体在空间中完全自由运动。 | 漂浮物、飞行的抛体。*(仅能声明在根 Body 下)* | + +--- + +## 2. 常用 XML 属性说明 + +编写 MJCF 模型文件时,`` 标签的常用属性配置如下: + +| 属性名称 | 默认值 | 说明与计算公式 | 官方文档跳转 | +| :--- | :--- | :--- | :--- | +| `name` | `""` | 关节的唯一名称,用于在 Python/C++ API 中进行 ID 寻址。 | [joint-name](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-name) | +| `type` | `hinge` | 关节类型,可选 `hinge`, `slide`, `ball`, `free`。 | [joint-type](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-type) | +| `pos` | `0 0 0` | 关节锚点在**当前 Body 坐标系**下的三维坐标。 | [joint-pos](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-pos) | +| `axis` | `0 0 1` | 运动轴线的矢量方向(仅对 `hinge` 和 `slide` 有效)。 | [joint-axis](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-axis) | +| `stiffness`| `0` | 关节弹簧刚度系数 $k$。产生的回弹力公式为: $f_{stiffness} = (ref - qpos) \times stiffness$ | [joint-stiffness](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-stiffness) | +| `damping` | `0` | 关节扭转阻尼系数 $c$。产生的阻尼力公式为: $f_{damping} = (0 - qvel) \times damping$ | [joint-damping](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-damping) | +| `frictionloss`| `0` | 关节干摩擦损失限额。用于模拟关节静摩擦与库伦摩擦阻力。 | [joint-frictionloss](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-frictionloss) | +| `armature` | `0` | 电枢惯量(转子转动惯量 $\times$ 减速比$^2$)。用于模拟电机转子的惯性。 | [joint-armature](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-armature) | +| `ref` | `0` | 关节弹簧的平衡位置偏置量(角度值或平移量)。 | [joint-ref](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-ref) | +| `range` | `0 0` | 运动范围限制。例如转动关节 `range="-1.57 1.57"` (单位为弧度或度)。 | [joint-range](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-range) | +| `limited` | `auto` | 是否启用运动限位。设置为 `true` 启用,`false` 禁用,`auto` 自动(依据 range 自动决定)。 | [joint-limited](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint-limited) | + +--- + +## 3. 经典动力学公式解析 + +MuJoCo 在仿真物理步进时,会将关节的弹簧、阻尼、电枢惯量等物理效应自动折算进**被动力**(`qfrc_passive`)中进行统一求解: + +### 3.1 弹簧恢复力 (Stiffness Force) +弹簧力拉引关节回到参考点 `ref`。 +$$f_{stiffness} = - \text{stiffness} \times (qpos - \text{ref})$$ + +### 3.2 阻抗力 (Damping Force) +阻尼力用于阻碍关节相对运动,稳定振荡。 +$$f_{damping} = - \text{damping} \times qvel$$ + +### 3.3 关节摩擦损失 (Friction Loss) +关节摩擦力属于非平滑约束,它由引擎的约束解算器(Constraint Solver)计算,以防止关节微小的滑动趋势。具体计算过程会体现在约束力 `efc_force` 中。 + +--- + +## 4. 实例分析 + +以下是本章示例模型 [scence.xml](file:///home/albusgive2/mujoco_learning/MJCF/Chapter4-joint/scence.xml) 中的双关节悬挂倒立摆场景定义: + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +* **水平转轴 `pivot`**:沿着 `axis="0 0 1"`(Z轴),配置了 `stiffness="0.5"`。只要它发生旋转偏移,弹簧就会产生扭矩强行拉回初始朝向。 +* **摆动轴 `ph`**:沿着 `axis="1 0 0"`(X轴),配置了阻尼 `damping="0.001"` 以平抑剧烈振荡,但没有设置刚度,允许倒立摆像真实单摆一样下垂和自由摆动。 + +--- + +## 5. 官方文档入口 +关于关节在 MuJoCo 中的计算原理与全部 XML 配置项,请参考: +* [MuJoCo 官方 XML 关节参考 (Joint XML Reference)](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint) +* [MuJoCo 官方计算与约束理论 (Computation Constraints)](https://mujoco.readthedocs.io/en/latest/computation/index.html#constraints) \ No newline at end of file diff --git a/MJCF/asset/2D_draw_point.png b/MJCF/asset/2D_draw_point.png deleted file mode 100644 index cdc7c2c..0000000 Binary files a/MJCF/asset/2D_draw_point.png and /dev/null differ diff --git a/MJCF/asset/compute_solimp.png b/MJCF/asset/compute_solimp.png deleted file mode 100644 index 2c8cf80..0000000 Binary files a/MJCF/asset/compute_solimp.png and /dev/null differ diff --git a/MJCF/asset/coompute_solref.png b/MJCF/asset/coompute_solref.png deleted file mode 100644 index 2cca1e9..0000000 Binary files a/MJCF/asset/coompute_solref.png and /dev/null differ diff --git a/MJCF/asset/coompute_solref2.png b/MJCF/asset/coompute_solref2.png deleted file mode 100644 index f05db94..0000000 Binary files a/MJCF/asset/coompute_solref2.png and /dev/null differ diff --git a/MJCF/asset/efc_force.png b/MJCF/asset/efc_force.png deleted file mode 100644 index 071d885..0000000 Binary files a/MJCF/asset/efc_force.png and /dev/null differ diff --git a/MJCF/asset/font_size.png b/MJCF/asset/font_size.png deleted file mode 100644 index d9408e3..0000000 Binary files a/MJCF/asset/font_size.png and /dev/null differ diff --git a/MJCF/asset/force.png b/MJCF/asset/force.png deleted file mode 100644 index f5da4c2..0000000 Binary files a/MJCF/asset/force.png and /dev/null differ diff --git a/MJCF/asset/frictionloss.png b/MJCF/asset/frictionloss.png deleted file mode 100644 index 20590ed..0000000 Binary files a/MJCF/asset/frictionloss.png and /dev/null differ diff --git a/MJCF/asset/initGeom.png b/MJCF/asset/initGeom.png deleted file mode 100644 index c06dcb9..0000000 Binary files a/MJCF/asset/initGeom.png and /dev/null differ diff --git a/MJCF/asset/joint.png b/MJCF/asset/joint.png deleted file mode 100644 index 035015d..0000000 Binary files a/MJCF/asset/joint.png and /dev/null differ diff --git a/MJCF/asset/mix_con.png b/MJCF/asset/mix_con.png deleted file mode 100644 index 2aa8249..0000000 Binary files a/MJCF/asset/mix_con.png and /dev/null differ diff --git a/MJCF/asset/mj_passive.png b/MJCF/asset/mj_passive.png deleted file mode 100644 index 118cdaa..0000000 Binary files a/MJCF/asset/mj_passive.png and /dev/null differ diff --git a/MJCF/asset/mujoco_doc_solimp.png b/MJCF/asset/mujoco_doc_solimp.png index 42146ae..23064e0 100644 Binary files a/MJCF/asset/mujoco_doc_solimp.png and b/MJCF/asset/mujoco_doc_solimp.png differ diff --git a/MJCF/asset/qfrc_bias.png b/MJCF/asset/qfrc_bias.png deleted file mode 100644 index 144277c..0000000 Binary files a/MJCF/asset/qfrc_bias.png and /dev/null differ diff --git a/MJCF/asset/ray.png b/MJCF/asset/ray.png deleted file mode 100644 index 479c675..0000000 Binary files a/MJCF/asset/ray.png and /dev/null differ diff --git a/MJCF/asset/soft_solver_param.png b/MJCF/asset/soft_solver_param.png deleted file mode 100644 index f0c2182..0000000 Binary files a/MJCF/asset/soft_solver_param.png and /dev/null differ diff --git a/MJCF/asset/xpos.png b/MJCF/asset/xpos.png deleted file mode 100644 index e170829..0000000 Binary files a/MJCF/asset/xpos.png and /dev/null differ diff --git a/Python/Chapter2-get_obj/tutorial.md b/Python/Chapter2-get_obj/tutorial.md index e6fa839..cbf6287 100644 --- a/Python/Chapter2-get_obj/tutorial.md +++ b/Python/Chapter2-get_obj/tutorial.md @@ -1,31 +1,105 @@ -# get obj -## 获取名字 -数量 - -**先看mjModel结构体开头部分,这里的命名方式都是nXXX,这代表各个元素的数量** - -## 获取id -`MJAPI int mj_name2id(const mjModel* m, int type, const char* name);` -**通过name获取实体的id -m :mjModel -type: mjmodel.h文件中的mjtObj中定义,这个是要获取id的实体类型一下是部分type类型枚举,在mjtObj中找到 -name: name -** - -## 获取位置 - -**可以通过 xpos和xxx_xpos获取各个对象的位置** -## 获取姿态 -**通过xquat可以获取body的姿态** - -我们可以和C++接口一样通过m.names中寻找各个实体的名字nXXX得到实体数量,name_xxxadr来寻找实体名字在names中的索引。 -在names中名字字符串之间通过”\x00”分割,name_xxxadr定位到的是该实体的第一个字符的位置,可以使用python的数组截取功能实现读取字符串,在寻找末尾的0来截取实体的实际名字。 +# 获取仿真世界中的实体信息 (Get Object Info) +在 MuJoCo 的 Python API 中,获取仿真世界中各种实体(Body, Joint, Geom 等)的数量、ID、位置及姿态非常简单。 + +--- + +## 1. 获取实体数量与名称 (Get Counts & Names) +所有的数量与命名信息都存储在 `mjModel` (Python 中为 `m`) 结构体中。 + +### 1.1 实体数量定义 +在 `mjModel` 中,定义了所有仿真元素的数量(以 `n` 开头): +```cpp +// 摘自 mujoco/mjmodel.h: 实体数量声明 +int nbody; // 身体 (Body) 数量 +int njnt; // 关节 (Joint) 数量 +int ngeom; // 几何体 (Geom) 数量 +int nsite; // 标记点 (Site) 数量 +int ncam; // 相机 (Camera) 数量 +int nlight; // 光源 (Light) 数量 +int nmesh; // 网格 (Mesh) 数量 +int nsensor; // 传感器 (Sensor) 数量 +int nactuator; // 驱动器 (Actuator) 数量 +``` +* 源码链接:[mujoco/mjmodel.h (sizes段)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +在 Python 中直接读取数量: +```python +print("Body 数量:", m.nbody) +print("Geom 数量:", m.ngeom) +``` + +### 1.2 实体名字解析与 Python 转换 +在底层,所有实体的名字都在一个大的一维字符缓冲区 `names` 中。Python 接口中暴露的 `name_xxxadr` 对应的是各实体名字在 `names` 大数组中的起始指针偏置。 +在 `names` 中,不同的名字之间使用空字符 `\x00`(在 Python 中表示为整数 `0`)进行分隔。因此在 Python 中可以通过以下切片方式截取出实体的名字: + +```python +# 获取 ID 为 1 的 Body 名字示例 +start_index = m.name_bodyadr[1] +name_bytes = m.names[start_index:] +for i in range(len(name_bytes)): + if name_bytes[i] == 0: # 遇到分隔符 \x00 截断 + name = name_bytes[:i].decode('utf-8') + break +print("Body[1] 名字:", name) +``` +* 源码链接:[mujoco/mjmodel.h (names段)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +--- + +## 2. 获取实体 ID (Get ID by Name) +因为 MuJoCo 底层物理计算全部基于数组索引(即 ID),在 API 中我们通常需要通过名字(string)获取其 ID。 +使用 API 函数: +```python +# Python 接口形式 +body_id = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_BODY, "robot_link1") +``` + +### 2.1 实体类型枚举 `mjtObj` +`mjtObj` 定义在 `mjmodel.h` 中,代表各种可能的实体类型: +```cpp +typedef enum mjtObj_ { + mjOBJ_UNKNOWN = 0, // 未知对象类型 + mjOBJ_BODY, // 身体 (Body) + mjOBJ_XBODY, // Body的替代表示,用于读取常规参考系 (而非惯性系) + mjOBJ_JOINT, // 关节 (Joint) + mjOBJ_DOF, // 自由度 (Degrees of freedom) + mjOBJ_GEOM, // 几何体 (Geom) + mjOBJ_SITE, // 标记点 (Site) + mjOBJ_CAMERA, // 相机 (Camera) + mjOBJ_LIGHT, // 光源 (Light) + mjOBJ_FLEX, // 柔性可变形体 (Flex) + mjOBJ_MESH, // 三角网格 (Mesh) + mjOBJ_SKIN, // 皮肤网格 (Skin) + mjOBJ_HFIELD, // 高度图 (Heightfield) + mjOBJ_TEXTURE, // 纹理 (Texture) + mjOBJ_MATERIAL, // 材质 (Material) + mjOBJ_PAIR, // 特殊碰撞 geom 对 (Pair) + mjOBJ_SENSOR // 传感器 (Sensor) +} mjtObj; +``` +* 源码链接:[mujoco/mjmodel.h (mjtObj定义)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjmodel.h) + +--- + +## 3. 获取位置与姿态 (Get Pos & Ori) +物体的实时笛卡尔空间状态(三维位置、姿态矩阵等)全部存储在 **`mjData`** (Python 中为 `d`) 中,由求解器在 `mj_step` 时进行实时更新更新: + +```cpp +// 摘自 mujoco/mjdata.h: 笛卡尔坐标状态 +mjtNum* xpos; // 各个 Body 的三维笛卡尔中心坐标 (nbody x 3) +mjtNum* xquat; // 各个 Body 的笛卡尔四元数旋转姿态 (nbody x 4) +mjtNum* xmat; // 各个 Body 的笛卡尔旋转矩阵姿态 (nbody x 9) +mjtNum* xipos; // 各个 Body 质心 (com) 的笛卡尔坐标 (nbody x 3) +mjtNum* ximat; // 各个 Body 质心的笛卡尔旋转矩阵 (nbody x 9) +``` +* 源码链接:[mujoco/mjdata.h (Cartesian state)](https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjdata.h) + +在 Python 中获取特定 body 实时三维位置与四元数姿态: ```python -name= m.names[m.name_bodyadr[1]:] -for i in range(len(name)): -if name[i] == 0: -name = name[:i] -break -print(name) +body_id = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_BODY, "support") +if body_id != -1: + pos = d.xpos[body_id] # 返回长度为 3 的 numpy 数组 + quat = d.xquat[body_id] # 返回长度为 4 的 numpy 姿态四元数 + print(f"位置: {pos}, 姿态: {quat}") ``` diff --git a/Python/Chapter3-sensor_data/tutorial.md b/Python/Chapter3-sensor_data/tutorial.md index e32d75f..932e473 100644 --- a/Python/Chapter3-sensor_data/tutorial.md +++ b/Python/Chapter3-sensor_data/tutorial.md @@ -1,9 +1,15 @@ # 传感器数据获取 - -**sensordata的索引需要依靠mjData的sensor_adr获取,这个可以使用sensor的id** -**这个我们要注意传感器具有的数据量,有的传感器是一个值,而有的传感器是三个值。我们可以使用mjModel中的sensor_dim获得传感器输出的参数量** -*演示:* +**`sensordata` 的索引需要依靠 `mjModel` 中的 `sensor_adr` 获取,也可以使用 sensor 的 id。** + +**我们要注意传感器具有的数据量,有的传感器是一个值,而有的传感器是三个值。我们可以使用 `mjModel` 中的 `sensor_dim` 获得传感器输出的参数量。** + +* `mjModel.sensor_adr` 的定义类似于: +```cpp +int* sensor_adr; // address in sensor array (nsensor x 1) +``` + +演示: ```python def get_sensor_data(sensor_name): @@ -15,9 +21,26 @@ def get_sensor_data(sensor_name): sensor_values = d.sensordata[start_idx : start_idx + dim] return sensor_values ``` -对于传感器其他的属性在 mjModel中可以直接获得。如下: - -**现在可以通过 MjData.sensor("sensorname").data 获取传感器数据** + +对于传感器的其他属性,在 `mjModel` 中可以直接获得: +```cpp +// mjModel 中的传感器相关成员变量说明 +int* sensor_type; // 传感器类型 (mjtSensor) (nsensor x 1) +int* sensor_datatype; // 数值数据类型 (mjtDataType) (nsensor x 1) +int* sensor_needstage; // 所需的计算阶段 (mjtStage) (nsensor x 1) +int* sensor_objtype; // 被测物体的类型 (mjtObj) (nsensor x 1) +int* sensor_objid; // 被测物体的 ID (nsensor x 1) +int* sensor_reftype; // 参考系物体的类型 (mjtObj) (nsensor x 1) +int* sensor_refid; // 参考系物体的 ID; -1: 全局参考系 (nsensor x 1) +int* sensor_dim; // 标量输出的数量 (nsensor x 1) +int* sensor_adr; // 传感器数据在 sensordata 中的地址 (nsensor x 1) +mjtNum* sensor_cutoff; // 实数/正数的截止值; 0: 忽略 (nsensor x 1) +mjtNum* sensor_noise; // 噪声音频标准差 (nsensor x 1) +mjtNum* sensor_user; // 用户数据 (nsensor x nuser_sensor) +int* sensor_plugin; // 插件实例 ID; -1: 非插件 (nsensor x 1) +``` + +**现在可以通过 `MjData.sensor("sensorname").data` 更方便地直接获取传感器数据。** ### 读取相机画面 相机来源一般是在模型文件中创建相机,或者创建一个相机手动控制,就像 base中 diff --git a/Python/Chapter4-draw/tutorial.md b/Python/Chapter4-draw/tutorial.md index 419fe95..7c06454 100644 --- a/Python/Chapter4-draw/tutorial.md +++ b/Python/Chapter4-draw/tutorial.md @@ -1,13 +1,45 @@ # 3D绘制 mujoco提供显示基础几何体和mujoco提供的一些特殊渲染几何体。查看文档可知mjv_initGeom函数能在渲染场景中增加几何体,mjv_connector可以用mjv_initGeom初始化的几何体绘制提供的一些特殊形状(如箭头,直线等)。mujoco显示画面的原理是通过mjv_updateScene 将仿真数据储存到mjvScene中,这是已经处理好的几何数据,接下来使用mjr_render传递给opengl渲染。我们在绘制过程中是要在仿真的几何数据处理完之后,加入绘制信息,再交给opengl渲染。 在mjvScene中添加信息,其实是直接在mjvScene的geoms后面续写,而且要增加ngeom长度。这里通过注释可以理解,mjvScene根据ngeom确定几何体数量再从geoms中获取资源。 - + +```c +struct _mjvScene { + // abstract geoms + int maxgeom; // size of allocated geom buffer + int ngeom; // number of geoms currently in buffer + mjvGeom* geoms; // buffer for geoms + int* geomorder; // buffer for ordering geoms by distance to camera + // ... +}; +typedef struct _mjvScene mjvScene; +``` + mjv_initGeom函数原型: - + +```c +void mjv_initGeom(mjvGeom* geom, int type, const mjtNum size[3], + const mjtNum pos[3], const mjtNum mat[9], const float rgba[4]); +``` + mjv_connector函数原型: - + +```c +void mjv_connector(mjvGeom* geom, int type, mjtNum width, + const mjtNum from[3], const mjtNum to[3]); +``` + geom是传入的仅绘制的几何体,需要使用mjv_initGeom初始化,type见下面,width是绘制的宽度,这个是对于渲染出来的画面的宽度,from起点,to终点 - + +```c +typedef enum _mjtGeom { + // ... + mjGEOM_ARROW = 100, // 箭头 + mjGEOM_ARROW1, // 无楔形箭头 + mjGEOM_ARROW2, // 双向箭头 + mjGEOM_LINE, // 直线 + // ... +} mjtGeom; +``` 这里是可以绘制的几何形状类型,分别是箭头,无楔形箭头,双向箭头,直线。 *演示——绘制几何体函数:* @@ -63,13 +95,24 @@ draw_arrow(pos_start2, end2, 0.1, rgba2) # 2D绘制 字体尺寸的初始化: - + +```c +typedef enum _mjtFontScale { + mjFONTSCALE_50 = 50, // 50% 缩放,适用于低分辨率渲染 + mjFONTSCALE_100 = 100, // 100% 缩放,默认普通比例 + mjFONTSCALE_150 = 150, // 150% 缩放 + mjFONTSCALE_200 = 200 // 200% 缩放 +} mjtFontScale; +``` + python: ```Python context = mujoco.MjrContext(m, mujoco.mjtFontScale.mjFONTSCALE_150) ``` -查阅文档我们可知2D绘制要在mjr_render之后进行 - +查阅文档我们可知,2D 绘制需要在此之后(例如 `mjr_render` 执行后)进行: + +> [!IMPORTANT] +> `mjr_render` 会在开始渲染时清除视口(Viewport)。因此,所有自定义的 2D 绘制操作(如绘制文本、矩形、表格或标签等)必须在调用 `mjr_render` **之后** 进行,否则绘制的内容会被视口清除覆盖。 *绘制文本:* diff --git a/Python/Chapter5-force/tutorial.md b/Python/Chapter5-force/tutorial.md index b7bfd3e..6958f0d 100644 --- a/Python/Chapter5-force/tutorial.md +++ b/Python/Chapter5-force/tutorial.md @@ -1,22 +1,47 @@ # 作用力 **平动** + $$ m \cdot a = F $$ + **旋转** + $$ I \cdot \alpha = \tau $$ + $$ F/\tau = 外部力+驱动力+被动力+约束力+偏置力 $$ # 外部力 我们查看文档计算部分可以看到有qfrc_passive,qfrc_actuator,qfrc_applied三个力分别对应被动力,驱动力,外部力 - +```c +mjtNum* qfrc_applied; // applied generalized force (nv x 1) +mjtNum* qfrc_passive; // passive generalized force (nv x 1) +mjtNum* qfrc_actuator; // actuator generalized force (nv x 1) +``` + 只要看手册的api或者头文件中,找到mj_applyFT函数应用外部力。 - + +```c +void mj_applyFT(const mjModel* m, mjData* d, + const mjtNum force[3], const mjtNum torque[3], + const mjtNum point[3], int body, + mjtNum* qfrc_target); +``` + 或者还可以使用xfrc_applied直接作用外部力在质心上。 - + +```c +mjtNum* xfrc_applied; // applied Cartesian forces (nbody x 6) +``` + **这里也说明了是笛卡尔力。** mj_applyFT函数的参数,是三维的力,三维扭矩,三维坐标(worldbody坐标系),bodyid。qfrc_target可以直接使用d->qfrc_applied。mj_applyFT是对于body在 **“自由度”** 上施加力。于是我们可以使用两个方式对body施加外部力。 qfrc_target还可以是以下这些被动力等qfrc_xxx的力 - + +```c +mjtNum* qfrc_passive; // passive generalized force (nv x 1) +mjtNum* qfrc_bias; // constraint bias generalized force (nv x 1) +mjtNum* qfrc_constraint; // constraint generalized force (nv x 1) +``` **mj_Data接口演示(作用在质心上):** ```C++ @@ -35,29 +60,65 @@ mj_applyFT(m, d, force, torque, point, id, d->qfrc_applied); **mj_applyFT每次调用都是增量式,如果我们想清除力可以使用mju_zero,如mju_zero(d->qfrc_applied, m->nv);** # 驱动力 - +```c +mjtNum* qfrc_actuator; // actuator generalized force (nv x 1) +``` mjData.qfrc_actuator是驱动器执行的力,不同驱动器最终会计算出力或者扭矩作用到关节上。 # 被动力 mjData.qfrc_passive是被动力,关节参数的damping,stiffness,摩擦力,流体阻力都会最终计算到改力中。 + $$ damping_force = (0-qvel)*damping $$ + $$ stiffness_force = (0-qpos)*stiffness $$ # 约束力 +```c +mjtNum* efc_force; // constraint force (nefc x 1) +``` mjData.efc_force是约束力,关节的frictionloss,equality计算出来的合力为改力。 - jar = Jac*qacc-aref 残差=雅可比*关节加速度-参考伪加速度 + 这里 $jar_0 = Jac \cdot qacc_0 - aref$ 表示无约束时的约束空间残差(未施加摩擦力时的相对加速度偏离量),$InverseConstraintMass$ 是该约束维度的质量倒数(即惯量响应矩阵 $A = J M^{-1} J^T + R$ 的对应对角元素)。 + +关节干摩擦力(静摩擦与滑动摩擦)的完整解算公式为: $$ -frictionlossforce = +frictionloss\_force = \begin{cases} - frictionloss, & \text{if } jar <= -InverseConstraintMass ⋅ floss \\ - -frictionloss, & \text{else if } jar>=InverseConstraintMass ⋅ floss \\ - (没看懂), & \text{else } + frictionloss, & \text{if } jar_0 \le -InverseConstraintMass \cdot floss & \text{(正向滑动摩擦)} \\ + -frictionloss, & \text{else if } jar_0 \ge InverseConstraintMass \cdot floss & \text{(反向滑动摩擦)} \\ + -\frac{jar_0}{InverseConstraintMass}, & \text{else } & \text{(静摩擦/粘滞状态,此时实际残差 } jar = 0\text{)} \end{cases} $$ + +**原理解析**: +1. **滑动摩擦阶段**:当外力推动关节的趋势(即 $jar_0$)超过了最大静摩擦力所能提供的阻碍加速度时,摩擦力饱和,大小恒为最大摩擦力 $floss$(即 `frictionloss`),方向与相对运动趋势相反。 +2. **静摩擦(粘滞)阶段**:当外力较小,在摩擦力能平衡的范围内(即 `else` 分支),摩擦力会自动产生一个恰好抵消相对运动趋势的力 $-\frac{jar_0}{InverseConstraintMass}$,使得最终的实际加速度残差 $jar$ 完美归零,即物体保持静止或相对匀速运动。 **源码实现(SRC/engine/engine_core_constraint.c)** - +```c +// linear friction limits +else if (floss && floss[i] > 0) { + // linear negative friction (正向滑动摩擦) + if (jar[i] <= -R[i] * floss[i]) { + force[i] = floss[i]; + state[i] = mjCNSTRSTATE_LINEARNEG; + } + + // linear positive friction (反向滑动摩擦) + else if (jar[i] >= R[i] * floss[i]) { + force[i] = -floss[i]; + state[i] = mjCNSTRSTATE_LINEARPOS; + } + + // quadratic/inactive friction (静摩擦/粘滞状态) + else { + force[i] = -jar[i] / R[i]; + state[i] = mjCNSTRSTATE_QUADRATIC; + } +} +``` # 偏置力 - +```c +mjtNum* qfrc_bias; // constraint bias generalized force (nv x 1) +``` mjData.qfrc_bias科里奥利力等,由引擎自动计算。 \ No newline at end of file diff --git a/Python/Chapter7-ray/tutorial.md b/Python/Chapter7-ray/tutorial.md index 0c8e218..eff5826 100644 --- a/Python/Chapter7-ray/tutorial.md +++ b/Python/Chapter7-ray/tutorial.md @@ -1,18 +1,53 @@ -# Ray -射线测距接口 - +# 射线检测 (Ray Casting) + +射线检测(Ray Casting)常用于模拟激光雷达、超声波测距传感器,或者进行视线、碰撞预判。在仿真运行到 `mj_kinematics` 或 `mj_fwdPosition` 之后方可调用(因为射线检测依赖实体的实时位置信息)。 + +--- + +## 1. 核心 API 说明 + +### 1.1 多束射线检测 `mj_multiRay` +在一处起点发射多条不同方向的射线,常用于激光雷达雷达阵列: +```cpp void mj_multiRay(const mjModel* m, mjData* d, const mjtNum pnt[3], const mjtNum* vec, const mjtByte* geomgroup, mjtByte flg_static, int bodyexclude, int* geomid, mjtNum* dist, int nray, mjtNum cutoff); -* m: mjModel -* d: mjData -* pnt: 射线起点 -* vec: 射线向量 -* geomgroup:检测分组,NULL代表检测所有 -* flg_static: 是否检测静态物体 1检测 0不检测 -* bodyexclude: -1 可用于指示包括所有主体 -* geomid: 检测到的几何体id,没有为-1 -* dist: 到geom表面的距离,数据是距离比例,是vec的倍数,无限远为-1 -* nray:射线数量 -* cutoff: 距离截断,超过截断距离的射线不检测,判断条件(pnt到geompos中心>cutoff+几何体外接球半径) -**其余ratXXX参数同理** \ No newline at end of file +``` + +### 1.2 单条射线检测 `mj_ray` +检测单个射线的交点: +```cpp +mjtNum mj_ray(const mjModel* m, const mjData* d, const mjtNum pnt[3], const mjtNum vec[3], + const mjtByte* geomgroup, mjtByte flg_static, int bodyexclude, + int geomid[1]); +``` + +### 1.3 其他特定几何类型检测 +* `mj_rayHfield`: 与高度图进行求交检测。 +* `mj_rayMesh`: 与三角网格模型(Mesh)求交检测。 +* `mju_rayGeom`: 用于纯几何计算(不受当前世界状态约束,计算指定位置尺寸的 geom)。 + +--- + +## 2. 关键参数详解 + +* **`pnt`** (`const mjtNum[3]`): 射线发射起点的 3D 笛卡尔坐标。 +* **`vec`** (`const mjtNum*`): 射线方向向量。 + * 对于 `mj_multiRay`,其为长度为 `nray * 3` 的数组,每 3 个元素代表一条射线的方向向量。 +* **`geomgroup`** (`const mjtByte*`): 几何体分组使能数组(长度为 `mjNGROUP`,通常为 5)。 + * 设为 `NULL` 代表检测所有 Geom 分组。若不为 `NULL`,则只有对应位为 1 的 Geom 组才会被检测。 +* **`flg_static`** (`mjtByte`): 是否检测静态(Static)物体。`1` 代表检测静态物体,`0` 代表忽略静态物体。 +* **`bodyexclude`** (`int`): 需要排除检测 of Body ID。 + * 设为 `-1` 代表检测所有 Body。 +* **`geomid`** (`int*`): 传出参数。检测到相交的 Geom ID。若未击中任何物体,则返回 `-1`。 +* **`dist`** (`mjtNum*`): 传出参数。到 Geom 表面的距离比例值。 + * **重要**:返回值不是绝对距离(单位米),而是**方向向量 `vec` 的缩放倍数**。即实际碰撞点坐标为: + $$pos_{hit} = pnt + dist \times vec$$ + * 若未发生碰撞,返回值为 `-1`。 +* **`nray`** (`int`): `mj_multiRay` 批量发射的射线总数。 +* **`cutoff`** (`mjtNum`): 距离截断阈值。如果起点到几何体中心点的距离大于 `cutoff` 加上该几何体的包围球半径,则直接跳过检测以提升性能。 + +--- + +## 3. 官方文档入口 +* [MuJoCo 官方 API 射线检测 (Ray Collisions)](https://mujoco.readthedocs.io/en/latest/programming.html#ray-collisions) \ No newline at end of file diff --git a/README.md b/README.md index 213d922..e714751 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,258 @@ MJCF为建模部分 CPP和python均为开发接口 extend为拓展和进阶 + +## 本地 Web 文档查看环境安装(可选) +如果只想在线浏览 GitHub 上的 Markdown,可以跳过本节。若希望在本机启动 Web 文档站查看教程,推荐使用 Python 3.10 或更新版本。项目已经提供 `pyproject.toml`,执行 `pip install -e .` 会以可编辑模式安装当前仓库,并自动安装 Python 版 MuJoCo 和本地文档站依赖。 + + +pip / venv 安装 + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +pip install -e . +``` + + + + +uv 安装 + +```bash +uv venv +source .venv/bin/activate +uv pip install -e . +``` + + + + +conda 安装 + +```bash +conda create -n mujoco-learning python=3.10 -y +conda activate mujoco-learning +python -m pip install --upgrade pip +pip install -e . +``` + +如果遇到 `GLIBCXX_x.x.xx not found` 之类的问题,可以在 conda 环境中尝试: + +```bash +conda install -c conda-forge libstdcxx-ng +``` + + + +安装完成后可以验证 MuJoCo viewer: + +```bash +python -m mujoco.viewer +``` + +也可以指定一个 MJCF 文件启动: + +```bash +python -m mujoco.viewer --mjcf=API-MJCF/mecanum.xml +``` + +更多 MuJoCo 安装说明见 [mujoco安装](MJCF/Chapter0-install/tutorial.md)。 + +## 本地文档站 +安装依赖后,在仓库根目录启动 FastAPI 文档站: + +```bash +mujoco_learning_doc +``` + +默认会从 `127.0.0.1:8000` 启动;如果 8000 已被占用,会自动尝试下一个可用端口。也可以手动指定端口: + +```bash +mujoco_learning_doc --port 8001 +``` + +或者使用环境变量: + +```bash +MUJOCO_DOCS_PORT=8001 mujoco_learning_doc +``` + +也可以直接使用 uvicorn: + +```bash +uvicorn mujoco_learning_doc.main:app --reload --host 127.0.0.1 --port 8000 +``` + +浏览器打开 即可查看本仓库的 Markdown 教程。文档站会自动读取 `README.md`、`directory.md` 以及各章节的 `tutorial.md` / `readme.md`,并支持目录导航、搜索、图片显示和代码高亮。 + +## Agent Skills (智能体技能安装) +本仓库在 `skills/` 下提供三个智能体技能(Skills): + +- `skills/mujoco-teaching`:教学型。用于向 Agent 提问 MuJoCo 概念、教程内容、学习路线和 API 原理。 +- `skills/mujoco-engineering`:工程型。用于让 Agent 复现示例、开发 MJCF/Python/C++ MuJoCo 功能、查找对应教程代码路径。 +- `skills/mujoco-cpp-build`:构建型。一键编译构建本仓库 C++ 实例与工具,支持源码编译版与 Release 依赖版,并在编译前主动向用户提问构建需求。 + +为了让不同平台的智能体(Agent)能识别和使用这些技能,我们提供了以下几种安装配置方式: + +### 1. Antigravity (Gemini Advanced Agentic Coding) +* **工作区自动加载**:当您在当前仓库根目录下运行 Antigravity 时,CLI 将自动扫描并加载 `skills/` 目录下的所有技能,无需额外安装。 +* **全局安装**:如果您在其他工作区也想让 Antigravity 调用这些技能,可将其复制到系统全局插件目录: + ```bash + mkdir -p ~/.gemini/config/skills + cp -r skills/mujoco-teaching skills/mujoco-engineering skills/mujoco-cpp-build ~/.gemini/config/skills/ + ``` + +### 2. Claude Code +* **工作区级适配**:Claude Code 会自动加载项目根目录下的 `.claude/skills/` 适配器。这些适配器已经配置好并指向了 `skills/` 下的 canonical 技能文件。 +* **全局安装**:如果要在其他目录下也能够使用,可将 canonical 目录直接安装至 Claude 的全局路径下: + ```bash + mkdir -p ~/.claude/skills + cp -r skills/mujoco-teaching skills/mujoco-engineering skills/mujoco-cpp-build ~/.claude/skills/ + ``` + +### 3. OpenCode +* **工作区级适配**:OpenCode 会自动加载项目根目录下的 `.opencode/skills/` 适配器。 +* **全局安装**:如果需要全局使用,复制技能文件夹至 OpenCode 全局技能目录: + ```bash + mkdir -p ~/.opencode/skills + cp -r skills/mujoco-teaching skills/mujoco-engineering skills/mujoco-cpp-build ~/.opencode/skills/ + ``` + +### 4. Codex +* **通过 skill-installer 从 GitHub 远程安装**: + 使用 Codex 的 `skill-installer` 直接从 GitHub 地址进行安装: + ```bash + skill-installer install https://github.com/Albusgive/mujoco_learning/tree/main/skills/mujoco-teaching + skill-installer install https://github.com/Albusgive/mujoco_learning/tree/main/skills/mujoco-engineering + skill-installer install https://github.com/Albusgive/mujoco_learning/tree/main/skills/mujoco-cpp-build + ``` +* **本地手动安装**:直接将技能文件夹拷贝至 Codex 本地技能存储路径: + ```bash + mkdir -p ~/.codex/skills + cp -r skills/mujoco-teaching skills/mujoco-engineering skills/mujoco-cpp-build ~/.codex/skills/ + ``` + +安装/复制完成后,请重启对应的命令行工具或 Agent 客户端。 + +--- + +## 跨工作区运行时的仓库路径设置 +如果智能体(如 Antigravity / Codex / Claude Code 等)未在当前仓库的工作区中运行,技能会在加载时尝试动态搜寻本仓库。首次找不到时,智能体主动询问您本仓库的克隆路径,并自动将其保存到已安装技能目录下的本地配置文件中: + +```text +/.local/repo_path.txt +``` + +`.local/repo_path.txt` 是普通路径文本文件。如果需要,您也可以通过运行技能中的 `find_repo.py` 脚本来手动设置或清除该缓存路径。不同系统设置方式如下: + + +macOS / Linux + +```bash +python ~/.codex/skills/mujoco-engineering/scripts/find_repo.py --set /path/to/mujoco_learning +python ~/.codex/skills/mujoco-teaching/scripts/find_repo.py --set /path/to/mujoco_learning +python ~/.codex/skills/mujoco-cpp-build/scripts/find_repo.py --set /path/to/mujoco_learning +``` + +清除保存路径: + +```bash +python ~/.codex/skills/mujoco-engineering/scripts/find_repo.py --clear +python ~/.codex/skills/mujoco-teaching/scripts/find_repo.py --clear +python ~/.codex/skills/mujoco-cpp-build/scripts/find_repo.py --clear +``` + + + + +Windows PowerShell + +```powershell +python $HOME\.codex\skills\mujoco-engineering\scripts\find_repo.py --set C:\path\to\mujoco_learning +python $HOME\.codex\skills\mujoco-teaching\scripts\find_repo.py --set C:\path\to\mujoco_learning +python $HOME\.codex\skills\mujoco-cpp-build\scripts\find_repo.py --set C:\path\to\mujoco_learning +``` + +清除保存路径: + +```powershell +python $HOME\.codex\skills\mujoco-engineering\scripts\find_repo.py --clear +python $HOME\.codex\skills\mujoco-teaching\scripts\find_repo.py --clear +python $HOME\.codex\skills\mujoco-cpp-build\scripts\find_repo.py --clear +``` + + + + +Windows CMD + +```bat +python %USERPROFILE%\.codex\skills\mujoco-engineering\scripts\find_repo.py --set C:\path\to\mujoco_learning +python %USERPROFILE%\.codex\skills\mujoco-teaching\scripts\find_repo.py --set C:\path\to\mujoco_learning +python %USERPROFILE%\.codex\skills\mujoco-cpp-build\scripts\find_repo.py --set C:\path\to\mujoco_learning +``` + +清除保存路径: + +```bat +python %USERPROFILE%\.codex\skills\mujoco-engineering\scripts\find_repo.py --clear +python %USERPROFILE%\.codex\skills\mujoco-teaching\scripts\find_repo.py --clear +python %USERPROFILE%\.codex\skills\mujoco-cpp-build\scripts\find_repo.py --clear +``` + + + +如果保存的路径失效,agent 会重新询问并覆盖保存。 + +环境变量 `MUJOCO_LEARNING_ROOT` 仍可作为可选备用方案。不同系统设置方式如下: + + +Windows PowerShell + +当前会话临时设置: + +```powershell +$env:MUJOCO_LEARNING_ROOT = "C:\path\to\mujoco_learning" +``` + +永久设置: + +```powershell +[Environment]::SetEnvironmentVariable("MUJOCO_LEARNING_ROOT", "C:\path\to\mujoco_learning", "User") +``` + + + + +Windows CMD + +当前会话临时设置: + +```bat +set MUJOCO_LEARNING_ROOT=C:\path\to\mujoco_learning +``` + +永久设置: + +```bat +setx MUJOCO_LEARNING_ROOT "C:\path\to\mujoco_learning" +``` + + + + +macOS / Linux + +```bash +export MUJOCO_LEARNING_ROOT=/path/to/mujoco_learning +``` + + + +两个 skill 也会自动尝试从当前工作区和常见克隆目录中寻找本仓库。 + ## 教程目录 🚀[教程目录(更新中)](directory.md) ## 视频教程 @@ -15,4 +267,3 @@ extend为拓展和进阶  ## 技术交流  - diff --git a/directory.md b/directory.md index c1cba7f..77cba29 100644 --- a/directory.md +++ b/directory.md @@ -1,97 +1,105 @@ ### MJCF模型文件 -> 第一节 mujoco安装 [install](MJCF/Chapter0-install/tutorial.md) -> 第二节 仿真世界调整 [virtual_world](MJCF/Chapter2-virtual_world/tutorial.md) +> 第一节 mujoco安装 [install](MJCF/Chapter0-install/tutorial.md) | [官方文档: 安装与构建](https://mujoco.readthedocs.io/en/latest/programming.html#building-mujoco-from-source) +> 第二节 仿真世界调整 [virtual_world](MJCF/Chapter2-virtual_world/tutorial.md) | [官方文档: option/visual/asset](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option) > - 物理世界参数,可视化配置,材质加载等 > -> 第三节 仿真世界 [worldbody](MJCF/Chapter3-worldbody/tutorial.md) +> 第三节 仿真世界 [worldbody](MJCF/Chapter3-worldbody/tutorial.md) | [官方文档: worldbody/body/geom](https://mujoco.readthedocs.io/en/latest/XMLreference.html#worldbody) > - 世界body,几何体,地形等 > -> 第四节 关节 [joint](MJCF/Chapter4-joint/tutorial.md) +> 第四节 关节 [joint](MJCF/Chapter4-joint/tutorial.md) | [官方文档: joint](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint) > - 关节类型,关节动力,关节参数等 > -> 第五节 摩擦力设置及计算方式 [friction](MJCF/Chapter5-friction/tutorial.md) +> 第五节 摩擦力设置及计算方式 [friction](MJCF/Chapter5-friction/tutorial.md) | [官方文档: friction/contact](https://mujoco.readthedocs.io/en/latest/XMLreference.html#pair) > - mujoco中多维度的摩擦力调整 > -> 第六节 驱动器 [actuator](MJCF/Chapter6-actuator/tutorial.md) +> 第六节 驱动器 [actuator](MJCF/Chapter6-actuator/tutorial.md) | [官方文档: actuator](https://mujoco.readthedocs.io/en/latest/XMLreference.html#actuator) > - 添加速度控制,位置控制,力矩控制等 > -> 第七节 灯光和复制 [light&replicate](MJCF/Chapter7-light&replicate/tutorial.md) +> 第七节 灯光和复制 [light&replicate](MJCF/Chapter7-light&replicate/tutorial.md) | [官方文档: light/camera](https://mujoco.readthedocs.io/en/latest/XMLreference.html#light) > - 灯光类型(和相机传感器关联性强) > - 复制实体,阵列排布,激光雷达演示 > -> 第八节 肌腱 [tendon](MJCF/Chapter8-tendon/tutorial.md) +> 第八节 肌腱 [tendon](MJCF/Chapter8-tendon/tutorial.md) | [官方文档: tendon](https://mujoco.readthedocs.io/en/latest/XMLreference.html#tendon) > - mujoco特有的驱动器和关节联系方式 > - 肌肉控制 > -> 第九节 传感器 [sensor](MJCF/Chapter9-sensor/tutorial.md) +> 第九节 传感器 [sensor](MJCF/Chapter9-sensor/tutorial.md) | [官方文档: sensor](https://mujoco.readthedocs.io/en/latest/XMLreference.html#sensor) > - 相机传感器,imu,速度,角度等 > -> 第十节 从CAD软件制作mjcf模型 [from_CAD_software](MJCF/Chapter10-from_CAD_software/tutorial.md) +> 第十节 从CAD软件制作mjcf模型 [from_CAD_software](MJCF/Chapter10-from_CAD_software/tutorial.md) | [官方文档: overview](https://mujoco.readthedocs.io/en/latest/overview.html#model-description) > - 以solidworks为例 > -> 第十一节 约束条件 [equality](MJCF/Chapter11-equality/tutorial.md) +> 第十一节 约束条件 [equality](MJCF/Chapter11-equality/tutorial.md) | [官方文档: equality](https://mujoco.readthedocs.io/en/latest/XMLreference.html#equality) > - 并联机构建模,驱动跟踪等 > -> 第十二节 默认属性设置 [default](MJCF/Chapter12-default/tutorial.md) +> 第十二节 默认属性设置 [default](MJCF/Chapter12-default/tutorial.md) | [官方文档: default](https://mujoco.readthedocs.io/en/latest/XMLreference.html#default) > - 几何体,body,关节等默认参数 > -> 第十三节 可变形体(老版3.2.7及以前) [composite](MJCF/Chapter13-composite/tutorial.md) +> 第十三节 可变形体(老版3.2.7及以前) [composite](MJCF/Chapter13-composite/tutorial.md) | [官方文档: composite](https://mujoco.readthedocs.io/en/latest/XMLreference.html#composite) > - 绳子,布料,软体等 > -> 第十四节 可变形体(新版3.3.0及以后) [flex](MJCF/Chapter14-flex/tutorial.md) +> 第十四节 可变形体(新版3.3.0及以后) [flex](MJCF/Chapter14-flex/tutorial.md) | [官方文档: flex](https://mujoco.readthedocs.io/en/latest/XMLreference.html#flex) > - 柔性材料,布料,软体,绳索,从网格构建可变形模型等 > -> 第十五节 关节帧 [keyframe](MJCF/Chapter15-keyframe/tutorial.md) +> 第十五节 关节帧 [keyframe](MJCF/Chapter15-keyframe/tutorial.md) | [官方文档: keyframe](https://mujoco.readthedocs.io/en/latest/XMLreference.html#keyframe) > - 加载和储存特定的姿态,关节信息等 ### API > 第一节 编译 > - 编译环境,编译命令,编译开发演示 -> > [make(C++)](CPP/Chapter1-make/tutorial.md) +> > [make(C++)](CPP/Chapter1-make/tutorial.md) | [官方文档: 编译与链接](https://mujoco.readthedocs.io/en/latest/programming.html#building-mujoco-from-source) > > 第二节 可视化和仿真进行 > - 仿真环境,可视化,仿真步进,仿真步进控制等 -> > [view&step(C++)](CPP/Chapter2-view&step/tutorial.md) -> > [view&step(Python)](Python/Chapter1-view&step/tutorial.md) +> > [view&step(C++)](CPP/Chapter2-view&step/tutorial.md) | [官方文档: 引擎初始化](https://mujoco.readthedocs.io/en/latest/programming.html#initialization) +> > [view&step(Python)](Python/Chapter1-view&step/tutorial.md) | [官方文档: 引擎初始化](https://mujoco.readthedocs.io/en/latest/programming.html#initialization) > > 第三节 获取仿真世界中的实体信息 > - 获取仿真世界中的实体信息,如名字,数量,参数信息等 -> > [get_obj(C++)](CPP/Chapter3-get_obj/tutorial.md) -> > [get_obj(Python)](Python/Chapter2-get_obj/tutorial.md) +> > [get_obj(C++)](CPP/Chapter3-get_obj/tutorial.md) | [官方文档: 实体索引与名称](https://mujoco.readthedocs.io/en/latest/programming.html#indices-and-names) +> > [get_obj(Python)](Python/Chapter2-get_obj/tutorial.md) | [官方文档: 实体索引与名称](https://mujoco.readthedocs.io/en/latest/programming.html#indices-and-names) > > 第四节 传感器数据获取 > - 获取仿真世界中的传感器数据,如相机,imu,速度,角度等 -> > [sensor_data(C++)](CPP/Chapter4-sensor_data/tutorial.md) -> > [sensor_data(Python)](Python/Chapter3-sensor_data/tutorial.md) +> > [sensor_data(C++)](CPP/Chapter4-sensor_data/tutorial.md) | [官方文档: 传感器读取](https://mujoco.readthedocs.io/en/latest/programming.html#sensors) +> > [sensor_data(Python)](Python/Chapter3-sensor_data/tutorial.md) | [官方文档: 传感器读取](https://mujoco.readthedocs.io/en/latest/programming.html#sensors) > > 第五节 2D和3D绘制 > - 2D绘制:文字,方形,表格等 > - 3D绘制:基础几何体,箭头等 -> > [draw(C++)](CPP/Chapter5-draw/tutorial.md) -> > [draw(Python)](Python/Chapter4-draw/tutorial.md) +> > [draw(C++)](CPP/Chapter5-draw/tutorial.md) | [官方文档: 自定义绘制](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) +> > [draw(Python)](Python/Chapter4-draw/tutorial.md) | [官方文档: 自定义绘制](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) > > 第六节 力的计算和API验证 > - mujoco中力是如何作用的及验证 -> > [force(C++)](CPP/Chapter6-force/tutorial.md) -> > [force(Python)](Python/Chapter5-force/tutorial.md) +> > [force(C++)](CPP/Chapter6-force/tutorial.md) | [官方文档: 动力学状态](https://mujoco.readthedocs.io/en/latest/programming.html#physics-state) +> > [force(Python)](Python/Chapter5-force/tutorial.md) | [官方文档: 动力学状态](https://mujoco.readthedocs.io/en/latest/programming.html#physics-state) > > 第七节 渲染配置 > - 双目相机,图像分割,渲染配置等 -> > [vis_cfg(C++)](CPP/Chapter7-vis_cfg/tutorial.md) -> > [vis_cfg(Python)](Python/Chapter6-vis_cfg/tutorial.md) +> > [vis_cfg(C++)](CPP/Chapter7-vis_cfg/tutorial.md) | [官方文档: 渲染与配置](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) +> > [vis_cfg(Python)](Python/Chapter6-vis_cfg/tutorial.md) | [官方文档: 渲染与配置](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) > > 第八节 射线测距 > - 测距传感器实现原理,自定义测距 -> > [ray(C++)](CPP/Chapter8-ray/tutorial.md) -> > [ray(Python)](Python/Chapter7-ray/tutorial.md) - +> > [ray(C++)](CPP/Chapter8-ray/tutorial.md) | [官方文档: 射线检测](https://mujoco.readthedocs.io/en/latest/programming.html#ray-collisions) +> > [ray(Python)](Python/Chapter7-ray/tutorial.md) | [官方文档: 射线检测](https://mujoco.readthedocs.io/en/latest/programming.html#ray-collisions) +> ### 拓展和进阶 > 触觉检测 > - 刚性触摸板和柔性材料触觉检测,通过api实现,并非插件方式 -> > [touch(C++&Python)](extend/touch/readme.md) +> > [touch(C++&Python)](extend/touch/readme.md) | [官方文档: 触觉传感器](https://mujoco.readthedocs.io/en/latest/XMLreference.html#sensor-touch) > > 软接触 > - mujoco中碰撞模型,如何通过调整动态“弹簧-阻尼”模型让模型展现不同材料的碰撞效果,对于碰撞和穿模问题如何调整 -> > [soft contact(mjcf&C++&Python)](extend/soft_contact/tutorial.md) +> > [soft contact(mjcf&C++&Python)](extend/soft_contact/tutorial.md) | [官方文档: 接触解算原理](https://mujoco.readthedocs.io/en/latest/computation/index.html#contacts) > -> 雷达/深度相机(基于ray) -> - 通过mujoco中的ray实现雷达和深度相机传感器,后续推广到mjx \ No newline at end of file +> Ray Caster(基于ray) +> - 通过mujoco中的ray实现雷达、深度相机和自定义测距传感器 +> > [ray caster](extend/deep_camera/readme.md) | [官方文档: 射线求交](https://mujoco.readthedocs.io/en/latest/programming.html#ray-collisions) +> > [独立仓库:Albusgive/mujoco_ray_caster](https://github.com/Albusgive/mujoco_ray_caster) +> +### 娱乐 +> 红石模拟 +> - 在 MuJoCo 中实现对 Minecraft(我的世界)红石电路及逻辑门的趣味三维仿真 +> > [红石电路仿真(Python)](fun/mujoco_red_stone/README.md) + diff --git a/extend/deep_camera/readme.md b/extend/deep_camera/readme.md index 7192359..339b6bc 100644 --- a/extend/deep_camera/readme.md +++ b/extend/deep_camera/readme.md @@ -1,4 +1,9 @@ -# Deep Camera -基于射线检测实现的深度测量,和mujoco自带测距传感器方式相同。 -## C++ +# Ray Caster + +基于 MuJoCo ray 实现的射线投射测距模块,可用于雷达、深度相机和自定义距离传感器。该方向已经独立整理到新仓库维护: + +[Albusgive/mujoco_ray_caster](https://github.com/Albusgive/mujoco_ray_caster) +本目录保留早期 C++ 示例和 XML 场景,后续 ray caster 相关更新请优先查看上面的独立仓库。 + +## C++ diff --git a/extend/equality/mjwarp_equality.py b/extend/equality/mjwarp_equality.py new file mode 100644 index 0000000..7842320 --- /dev/null +++ b/extend/equality/mjwarp_equality.py @@ -0,0 +1,253 @@ +# Copyright 2025 The Newton Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""mjwarp-viewer: load and simulate an MJCF with MuJoCo Warp. + +Usage: mjwarp-viewer [flags] + +Example: + mjwarp-viewer benchmark/humanoid/humanoid.xml -o "opt.solver=cg" +""" + +import copy +import enum +import logging +import shutil +import sys +import time +from typing import Sequence + +import mujoco +import mujoco.viewer +import numpy as np +import warp as wp +from absl import app +from absl import flags +from etils import epath + +import mujoco_warp as mjw + +# mjwarp-viewer has priviledged access to a few internal methods +from mujoco_warp._src.io import find_keys +from mujoco_warp._src.io import make_trajectory +from mujoco_warp._src.io import override_model + + +class EngineOptions(enum.IntEnum): + """Engine option.""" + + WARP = 0 + C = 1 + + +_CLEAR_WARP_CACHE = flags.DEFINE_bool("clear_warp_cache", False, "Clear warp caches (kernel, LTO, CUDA compute)") +_ENGINE = flags.DEFINE_enum_class("engine", EngineOptions.WARP, EngineOptions, "Simulation engine") +_NCONMAX = flags.DEFINE_integer("nconmax", None, "Maximum number of contacts.") +_NJMAX = flags.DEFINE_integer("njmax", None, "Maximum number of constraints per world.") +_NCCDMAX = flags.DEFINE_integer("nccdmax", None, "Maximum number of CCD contacts per world.") +_OVERRIDE = flags.DEFINE_multi_string("override", [], "Model overrides (notation: foo.bar = baz)", short_name="o") +_KEYFRAME = flags.DEFINE_integer("keyframe", 0, "keyframe to initialize simulation.") +_DEVICE = flags.DEFINE_string("device", None, "override the default Warp device") +_REPLAY = flags.DEFINE_string("replay", None, "keyframe sequence to replay, keyframe name must prefix match") + +_VIEWER_GLOBAL_STATE = {"running": True, "step_once": False, "eq_active": True, "reset_active": False} + + +def key_callback(key: int) -> None: + if key == 32: # Space bar + _VIEWER_GLOBAL_STATE["eq_active"] = not _VIEWER_GLOBAL_STATE["eq_active"] + logging.info("EQ_ACTIVE = %s", _VIEWER_GLOBAL_STATE["eq_active"]) + elif key == 259: # Backspace + _VIEWER_GLOBAL_STATE["reset_active"] = True + elif key == 82: # 'r' + _VIEWER_GLOBAL_STATE["running"] = not _VIEWER_GLOBAL_STATE["running"] + logging.info("RUNNING = %s", _VIEWER_GLOBAL_STATE["running"]) + elif key == 46: # period + _VIEWER_GLOBAL_STATE["step_once"] = True + + +def _load_model(path: epath.Path) -> mujoco.MjModel: + if not path.exists(): + resource_path = epath.resource_path("mujoco_warp") / path + if not resource_path.exists(): + raise FileNotFoundError(f"file not found: {path}\nalso tried: {resource_path}") + path = resource_path + + print(f"Loading model from: {path}...") + if path.suffix == ".mjb": + return mujoco.MjModel.from_binary_path(path.as_posix()) + + spec = mujoco.MjSpec.from_file(path.as_posix()) + # check if the file has any mujoco.sdf test plugins + if any(p.plugin_name.startswith("mujoco.sdf") for p in spec.plugins): + from mujoco_warp.test_data.collision_sdf.utils import register_sdf_plugins as register_sdf_plugins + + register_sdf_plugins(mjw) + return spec.compile() + + +def _compile_step(m, d): + print("Compiling physics step...", end="", flush=True) + start = time.time() + # capture the whole step function as a CUDA graph + with wp.ScopedCapture() as capture: + mjw.step(m, d) + elapsed = time.time() - start + print(f"done ({elapsed:0.2g}s).") + return capture.graph + + +def _main(argv: Sequence[str]) -> None: + """Runs viewer app.""" + if len(argv) < 2: + argv.append("equality.xml") + elif len(argv) > 2: + raise app.UsageError("Too many command-line arguments.") + + mjm = _load_model(epath.Path(argv[1])) + mjd = mujoco.MjData(mjm) + + # Set initial velocity to shoot the box complex towards the wall and rotate it + body_id = mujoco.mj_name2id(mjm, mujoco.mjtObj.mjOBJ_BODY, 'box000') + joint_id = mjm.body_jntadr[body_id] + qvel_adr = mjm.jnt_dofadr[joint_id] + mjd.qvel[qvel_adr : qvel_adr + 6] = [80.0, 0.0, 0.0, 0.0, 10.0, 0.0] + + # Get sensor id for wall force + sensor_id = mujoco.mj_name2id(mjm, mujoco.mjtObj.mjOBJ_SENSOR, 'wall_force') + + ctrls = None + ctrlid = 0 + if _REPLAY.value: + keys = find_keys(mjm, _REPLAY.value) + if not keys: + raise app.UsageError(f"Key prefix not find: {_REPLAY.value}") + ctrls = make_trajectory(mjm, keys) + mujoco.mj_resetDataKeyframe(mjm, mjd, keys[0]) + elif mjm.nkey > 0 and _KEYFRAME.value > -1: + mujoco.mj_resetDataKeyframe(mjm, mjd, _KEYFRAME.value) + + if _ENGINE.value == EngineOptions.C: + override_model(mjm, _OVERRIDE.value) + print( + f" nbody: {mjm.nbody} nv: {mjm.nv} ngeom: {mjm.ngeom} nu: {mjm.nu}\n" + f" solver: {mujoco.mjtSolver(mjm.opt.solver).name} cone: {mujoco.mjtCone(mjm.opt.cone).name}" + f" iterations: {mjm.opt.iterations} ls_iterations: {mjm.opt.ls_iterations}\n" + f" integrator: {mujoco.mjtIntegrator(mjm.opt.integrator).name}\n" + ) + print(f"MuJoCo C simulating with dt = {mjm.opt.timestep:.3f}...") + else: + wp.config.quiet = flags.FLAGS["verbosity"].value < 1 + wp.init() + wp.set_device(_DEVICE.value) + if _CLEAR_WARP_CACHE.value: + wp.clear_kernel_cache() + wp.clear_lto_cache() + # Clear CUDA compute cache for truly cold start JIT + compute_cache = epath.Path("~/.nv/ComputeCache").expanduser() + if compute_cache.exists(): + shutil.rmtree(compute_cache) + compute_cache.mkdir() + + override_model(mjm, _OVERRIDE.value) + m = mjw.put_model(mjm) + override_model(m, _OVERRIDE.value) + d = mjw.put_data(mjm, mjd, nconmax=_NCONMAX.value, njmax=_NJMAX.value, nccdmax=_NCCDMAX.value) + wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)])) + wp.copy(d.eq_active, wp.array([mjd.eq_active[:len(d.eq_active)].astype(np.int32)])) + graph = _compile_step(m, d) if wp.get_device().is_cuda else None + if graph is None: + mjw.step(m, d) # warmup step + print("Running Warp unoptimized on CPU.") + broadphase, filter = mjw.BroadphaseType(m.opt.broadphase).name, mjw.BroadphaseFilter(m.opt.broadphase_filter).name + solver, cone = mjw.SolverType(m.opt.solver).name, mjw.ConeType(m.opt.cone).name + integrator = mjw.IntegratorType(m.opt.integrator).name + iterations, ls_iterations = m.opt.iterations, m.opt.ls_iterations + ls_str = f"{'parallel' if m.opt.ls_parallel else 'iterative'} linesearch iterations: {ls_iterations}" + print( + f" nbody: {m.nbody} nv: {m.nv} ngeom: {m.ngeom} nu: {m.nu} is_sparse: {m.is_sparse}\n" + f" broadphase: {broadphase} broadphase_filter: {filter}\n" + f" solver: {solver} cone: {cone} iterations: {iterations} {ls_str}\n" + f" integrator: {integrator} graph_conditional: {m.opt.graph_conditional}" + ) + print(f"Data\n nworld: {d.nworld} nconmax: {int(d.naconmax / d.nworld)} njmax: {d.njmax}\n") + print(f"MuJoCo Warp simulating with dt = {m.opt.timestep.numpy()[0]:.3f}...") + + with mujoco.viewer.launch_passive(mjm, mjd, key_callback=key_callback) as viewer: + opt = copy.copy(mjm.opt) + + while True: + start = time.time() + + if ctrls is not None and ctrlid < len(ctrls): + mjd.ctrl[:] = ctrls[ctrlid] + ctrlid += 1 + + if _ENGINE.value == EngineOptions.C: + mujoco.mj_step(mjm, mjd) + # Add equality logic + wall_force = mjd.sensordata[sensor_id : sensor_id + 3] + if not _VIEWER_GLOBAL_STATE["eq_active"] and np.linalg.norm(wall_force) > 0.01: + mjd.eq_active[:] = 0 + else: # mjwarp + wp.copy(d.ctrl, wp.array([mjd.ctrl.astype(np.float32)])) + wp.copy(d.act, wp.array([mjd.act.astype(np.float32)])) + wp.copy(d.xfrc_applied, wp.array([mjd.xfrc_applied.astype(np.float32)])) + wp.copy(d.qpos, wp.array([mjd.qpos.astype(np.float32)])) + wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)])) + wp.copy(d.time, wp.array([mjd.time], dtype=wp.float32)) + # if the user changed an option in the MuJoCo Simulate UI, go ahead and recompile the step + # TODO: update memory tied to option max iterations + if mjm.opt != opt: + opt = copy.copy(mjm.opt) + m = mjw.put_model(mjm) + graph = _compile_step(m, d) if wp.get_device().is_cuda else None + if _VIEWER_GLOBAL_STATE["running"] or _VIEWER_GLOBAL_STATE["step_once"]: + _VIEWER_GLOBAL_STATE["step_once"] = False + if graph is None: + mjw.step(m, d) + else: + wp.capture_launch(graph) + wp.synchronize() + mjw.get_data_into(mjd, mjm, d) + # Add equality logic + wall_force = mjd.sensordata[sensor_id : sensor_id + 3] + if not _VIEWER_GLOBAL_STATE["eq_active"] and np.linalg.norm(wall_force) > 0.01: + mjd.eq_active[:] = 0 + wp.copy(d.eq_active, wp.array([mjd.eq_active.astype(np.int32)])) + + viewer.sync() + + if _VIEWER_GLOBAL_STATE["reset_active"]: + mjd.qvel[qvel_adr : qvel_adr + 6] = [80.0, 0.0, 0.0, 0.0, 10.0, 0.0] + if _ENGINE.value != EngineOptions.C: + wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)])) + _VIEWER_GLOBAL_STATE["reset_active"] = False + + elapsed = time.time() - start + if elapsed < mjm.opt.timestep: + time.sleep(mjm.opt.timestep - elapsed) + + +def main(): + # absl flags assumes __main__ is the main running module for printing usage documentation + # pyproject bin scripts break this assumption, so manually set argv and docstring + sys.argv[0] = "mujoco_warp.viewer" + sys.modules["__main__"].__doc__ = __doc__ + app.run(_main) + + +if __name__ == "__main__": + main() diff --git a/extend/soft_contact/tutorial.md b/extend/soft_contact/tutorial.md index b44e3e9..f232f49 100644 --- a/extend/soft_contact/tutorial.md +++ b/extend/soft_contact/tutorial.md @@ -2,11 +2,27 @@ *** 软接触由geom中的solimp和solref参数调控*** 虽然geom在mujoco中是刚体,但是通过soft contact可以近似现实中物体碰撞时发生的形变,比如刚性比较强的物体发生的微小变形,又或者是一个可以通过缓冲力的物体,又或者是一个很有弹性的橡皮球,这些在mujoco的刚体中可以通过soft contact近似出这些物体的碰撞情况  - + +| 属性名称 | 参数类型 | 默认值 | 物理含义 (正数格式) | 物理含义 (负数格式) | +| :--- | :--- | :--- | :--- | :--- | +| **`solref`** | `mjtNum[2]` | `0.02, 1.0` | `(timeconst, dampratio)`• `timeconst`: 时间常数,控制回弹反应速度• `dampratio`: 阻尼比,控制回弹振荡幅度(1 为临界阻尼) | `(-stiffness, -damping)`• `stiffness`: 直接指定虚拟弹簧刚度 $k$• `damping`: 直接指定虚拟弹簧阻尼 $b$ | +| **`solimp`** | `mjtNum[5]` | `0.9, 0.95, 0.001, 0.5, 2` | `(dmin, dmax, width, midpoint, power)`• `dmin`: 阻抗的最小值• `dmax`: 阻抗的最大值• `width`: 过渡区域的穿透深度宽度• `midpoint`: 分段过渡曲线的分界点 (0 到 1 之间)• `power`: 曲线的幂次 | - | + [公式计算可视化(desmos)](https://www.desmos.com/calculator/irtgrwjpkb?lang=zh-CN) + +### 接触参数及求解曲线实时可视化工具 +> 💡 **提示**:本工具为交互式 Web 绘图组件。如果你正在直接阅读 Markdown 源文件,此处的交互组件仅在本地部署的 Web 文档服务中渲染运行。 +> - 如果希望进行交互式参数调节,请在终端运行文档服务后通过浏览器访问本页面。 +> - 你也可以直接点击访问在线的 [Desmos 交互式计算与波形绘制页面](https://www.desmos.com/calculator/irtgrwjpkb?lang=zh-CN)。 + + + **在这里把碰撞拆解成了下面公式** + $$a_{ref}=-bv-kr$$ + $$a_{1}=(1-d) \cdot a_{0}-d \cdot a_{ref}$$ + > a1:计算之后的加速度 > a0:无约束时的加速度 > v :速度 @@ -25,8 +41,11 @@ $$a_{1}=(1-d) \cdot a_{0}-d \cdot a_{ref}$$ >- power:控制d变化曲线,会使曲线变化的“更快” **计算公式** + $$x_{\text{normal}} = \frac{|r|}{\text{width}}$$ + $$a = \frac{1}{\text{midpoint}^{\text{power}-1}}$$ + $$b = \frac{1}{(1 - \text{midpoint})^{\text{power}-1}}$$ $$Y(x) = \{ @@ -46,7 +65,54 @@ $$d\left(x_{normal}\right) = d_{0} + Y\left(x_{normal}\right) \left( d_{\text{wi **源码位置** engine/engine_core_constraint.c: static void getimpedance(const mjtNum* solimp, mjtNum pos, mjtNum margin,mjtNum* imp, mjtNum* impP) - +```c +static void getimpedance(const mjtNum* solimp, mjtNum pos, mjtNum margin, mjtNum* imp, mjtNum* impP) { + // flat function + if (solimp[0] == solimp[1] || solimp[2] <= mjMINVAL) { + *imp = 0.5*(solimp[0] + solimp[1]); + *impP = 0; + return; + } + + // x = abs((pos-margin) / width) + mjtNum x = (pos-margin) / solimp[2]; + mjtNum sgn = 1; + if (x < 0) { + x = -x; + sgn = -1; + } + + // clamp x to [0, 1] + if (x >= 1) { + *imp = solimp[1]; + *impP = 0; + return; + } + + // linear or power transition + mjtNum y, yP; + if (solimp[4] == 1) { + y = x; + yP = 1; + } + // y(x) = a*x^p if x<=midpoint + else if (x <= solimp[3]) { + mjtNum a = 1 / mju_pow(solimp[3], solimp[4]-1); + y = a * mju_pow(x, solimp[4]); + yP = solimp[4] * a * mju_pow(x, solimp[4]-1); + } + // y(x) = 1-b*(1-x)^p if x>midpoint + else { + mjtNum b = 1 / mju_pow(1-solimp[3], solimp[4]-1); + y = 1 - b * mju_pow(1-x, solimp[4]); + yP = solimp[4] * b * mju_pow(1-x, solimp[4]-1); + } + + // scale + *imp = solimp[0] + y*(solimp[1]-solimp[0]); + *impP = yP * sgn * (solimp[1]-solimp[0]) / solimp[2]; +} +``` ## solref参数 **这个参数影响公式中的k,b** @@ -56,7 +122,9 @@ static void getimpedance(const mjtNum* solimp, mjtNum pos, mjtNum margin,mjtNum* >- dampratio:会影响k,数值越小k越大,一般设置为1,数值过小会阻尼不够或弹性不足,数值过大会约束过过度 **计算公式** + $$b=\frac{2}{d_{width} \cdot timeconst}$$ + $$k=\frac{d(r)}{d_{width} \cdot timeconst^2 \cdot dampratio^2}$$ > 参数为负值(-stiffness,-damping) @@ -64,7 +132,9 @@ $$k=\frac{d(r)}{d_{width} \cdot timeconst^2 \cdot dampratio^2}$$ >- damping:与d,dwidth一起影响b值 **计算公式** + $$b=\frac{damping}{d_{width}}$$ + $$k=\frac{stiffness \cdot d(r)}{d_{width}^2}$$ [**desmos**](https://www.desmos.com/calculator/irtgrwjpkb?lang=zh-CN) @@ -72,8 +142,38 @@ $$k=\frac{stiffness \cdot d(r)}{d_{width}^2}$$ **源码位置** engine/engine_core_constraint.c: void mj_makeImpedance(const mjModel* m, mjData* d) - - +```c +// ... inside mj_makeImpedance +// set R and KBIP for all constraint dimensions +for (int j=0; j < dim; j++) { + // ... + // friction: K = 0 + if (tp == mjCNSTR_FRICTION_DOF || tp == mjCNSTR_FRICTION_TENDON || elliptic_friction) { + KBIP[4*(i+j)] = 0; + } + // standard: K = 1 / (d_width^2 * timeconst^2 * dampratio^2) + else if (ref[0] > 0) { + KBIP[4*(i+j)] = 1 / mju_max(mjMINVAL, solimp[1]*solimp[1] * ref[0]*ref[0] * ref[1]*ref[1]); + } + // direct: K = -solref[0] / d_width^2 + else { + KBIP[4*(i+j)] = -ref[0] / mju_max(mjMINVAL, solimp[1]*solimp[1]); + } + + // standard: B = 2 / (d_width*timeconst) + if (ref[1] > 0) { + KBIP[4*(i+j)+1] = 2 / mju_max(mjMINVAL, solimp[1]*ref[0]); + } + // direct: B = -solref[1] / d_width + else { + KBIP[4*(i+j)+1] = -ref[1] / mju_max(mjMINVAL, solimp[1]); + } + + // I = imp, P = imp' + KBIP[4*(i+j)+2] = imp; + KBIP[4*(i+j)+3] = impP; +} +``` ## solimp和solref的混合规则 > 情况一:根据priority的大小,选择两个碰撞geom中priority大的solimp和solref参数 @@ -83,12 +183,39 @@ void mj_makeImpedance(const mjModel* m, mjData* d) 源码位置:engine/engine_collision_driver.c: mj_contactParam - +```c +// compute solver mix factor +mjtNum mix; +if (solmix1 >= mjMINVAL && solmix2 >= mjMINVAL) { + mix = solmix1 / (solmix1 + solmix2); +} else if (solmix1 < mjMINVAL && solmix2 < mjMINVAL) { + mix = 0.5; +} else if (solmix1 < mjMINVAL) { + mix = 0.0; +} else { + mix = 1.0; +} + +// reference standard: mix +if (solref1[0] > 0 && solref2[0] > 0) { + for (int i=0; i < mjNREF; i++) { + solref[i] = mix*solref1[i] + (1-mix)*solref2[i]; + } +} +// reference direct: min +else { + for (int i=0; i < mjNREF; i++) { + solref[i] = mju_min(solref1[i], solref2[i]); + } +} +``` ## 调整思路 **可以从pd控制器和碰撞曲线两个方面分析碰撞** ### PD + $$a_{ref}=-bv-kr$$ + 由这个公式可以分析出,如果我们想抑制陷入深度(穿模),那需要增大刚度k的参数,如果碰撞弹性很大或者是接触时抖动剧烈,可能是阻尼b不够 ### 碰撞曲线 碰撞曲线计算出d参数,会根据陷入深度动态调节pd控制器的比例 diff --git a/extend/mujoco_red_stone/.vscode/settings.json b/fun/mujoco_red_stone/.vscode/settings.json similarity index 100% rename from extend/mujoco_red_stone/.vscode/settings.json rename to fun/mujoco_red_stone/.vscode/settings.json diff --git a/extend/mujoco_red_stone/Assumptions.mp3 b/fun/mujoco_red_stone/Assumptions.mp3 similarity index 100% rename from extend/mujoco_red_stone/Assumptions.mp3 rename to fun/mujoco_red_stone/Assumptions.mp3 diff --git a/extend/mujoco_red_stone/README.md b/fun/mujoco_red_stone/README.md similarity index 100% rename from extend/mujoco_red_stone/README.md rename to fun/mujoco_red_stone/README.md diff --git a/extend/mujoco_red_stone/carved_pumpkin.png b/fun/mujoco_red_stone/carved_pumpkin.png similarity index 100% rename from extend/mujoco_red_stone/carved_pumpkin.png rename to fun/mujoco_red_stone/carved_pumpkin.png diff --git a/extend/mujoco_red_stone/iron_block.png b/fun/mujoco_red_stone/iron_block.png similarity index 100% rename from extend/mujoco_red_stone/iron_block.png rename to fun/mujoco_red_stone/iron_block.png diff --git a/extend/mujoco_red_stone/pumpkin_side.png b/fun/mujoco_red_stone/pumpkin_side.png similarity index 100% rename from extend/mujoco_red_stone/pumpkin_side.png rename to fun/mujoco_red_stone/pumpkin_side.png diff --git a/extend/mujoco_red_stone/pumpkin_top.png b/fun/mujoco_red_stone/pumpkin_top.png similarity index 100% rename from extend/mujoco_red_stone/pumpkin_top.png rename to fun/mujoco_red_stone/pumpkin_top.png diff --git a/extend/mujoco_red_stone/red_stone.py b/fun/mujoco_red_stone/red_stone.py similarity index 100% rename from extend/mujoco_red_stone/red_stone.py rename to fun/mujoco_red_stone/red_stone.py diff --git a/extend/mujoco_red_stone/scence.xml b/fun/mujoco_red_stone/scence.xml similarity index 100% rename from extend/mujoco_red_stone/scence.xml rename to fun/mujoco_red_stone/scence.xml diff --git a/extend/mujoco_red_stone/sea.png b/fun/mujoco_red_stone/sea.png similarity index 100% rename from extend/mujoco_red_stone/sea.png rename to fun/mujoco_red_stone/sea.png diff --git a/mujoco_learning_doc/__init__.py b/mujoco_learning_doc/__init__.py new file mode 100644 index 0000000..01ba5de --- /dev/null +++ b/mujoco_learning_doc/__init__.py @@ -0,0 +1 @@ +"""Local documentation web app for the MuJoCo tutorial repository.""" diff --git a/mujoco_learning_doc/main.py b/mujoco_learning_doc/main.py new file mode 100644 index 0000000..947e314 --- /dev/null +++ b/mujoco_learning_doc/main.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +import argparse +import os +import re +import socket +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable +from urllib.parse import quote, unquote, urlsplit, urlunsplit + +import markdown +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import FileResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + + +APP_DIR = Path(__file__).resolve().parent +REPO_ROOT = APP_DIR.parent +STATIC_DIR = APP_DIR / "static" +TEMPLATES_DIR = APP_DIR / "templates" + +DOC_FILENAMES = {"README.md", "readme.md", "tutorial.md", "directory.md"} +ROOT_DOCS = ("README.md", "directory.md") +NATURAL_SORT_RE = re.compile(r"(\d+)") +MARKDOWN_EXTENSIONS = [ + "extra", + "toc", + "tables", + "fenced_code", + "codehilite", + "pymdownx.arithmatex", + "sane_lists", +] +LINK_RE = re.compile(r"(!?\[[^\]]*?\]\()([^)]+)(\))") +HTML_LINK_RE = re.compile(r"""((?:src|href)=["'])([^"']+)(["'])""", re.IGNORECASE) + + +@dataclass(frozen=True) +class DocItem: + title: str + path: str + section: str + url: str + + +@dataclass(frozen=True) +class NavGroup: + key: str + title: str + items: list[DocItem] + is_open: bool + + +app = FastAPI(title="MuJoCo 教程文档站") +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +templates = Jinja2Templates(directory=TEMPLATES_DIR) + + +def port_is_available(host: str, port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind((host, port)) + except OSError: + return False + return True + + +def find_available_port(host: str, start_port: int, attempts: int = 20) -> int: + for port in range(start_port, start_port + attempts): + try: + if port_is_available(host, port): + return port + except PermissionError: + return start_port + raise RuntimeError(f"No available port found from {start_port} to {start_port + attempts - 1}.") + + +def run() -> None: + import uvicorn + + parser = argparse.ArgumentParser(description="Start the local MuJoCo tutorial docs site.") + parser.add_argument("--host", default=os.environ.get("MUJOCO_DOCS_HOST", "127.0.0.1")) + parser.add_argument( + "--port", + type=int, + default=int(os.environ.get("MUJOCO_DOCS_PORT", "8000")), + help="Preferred port. If it is busy, the next free port will be used.", + ) + parser.add_argument("--no-reload", action="store_true", help="Disable uvicorn reload mode.") + args = parser.parse_args() + + port = find_available_port(args.host, args.port) + if port != args.port: + print(f"Port {args.port} is in use, using {port} instead.") + print(f"Docs site: http://{args.host}:{port}") + uvicorn.run("mujoco_learning_doc.main:app", host=args.host, port=port, reload=not args.no_reload) + + +def safe_repo_path(relative_path: str) -> Path: + decoded = unquote(relative_path).strip("/") + candidate = (REPO_ROOT / decoded).resolve() + try: + candidate.relative_to(REPO_ROOT) + except ValueError as exc: + raise HTTPException(status_code=404, detail="File not found") from exc + return candidate + + +def web_path(path: str) -> str: + return quote(path.replace("\\", "/"), safe="/") + + +def title_from_markdown(path: Path) -> str: + try: + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if stripped.startswith("#"): + return stripped.lstrip("#").strip() or path.parent.name + except UnicodeDecodeError: + return path.stem + return path.parent.name if path.name.lower() in {"tutorial.md", "readme.md"} else path.stem + + +def section_for(path: Path) -> str: + relative = path.relative_to(REPO_ROOT) + if len(relative.parts) == 1: + return "首页" + return relative.parts[0] + + +def section_title(section: str) -> str: + titles = { + "MJCF": "MJCF建模", + "Python": "Python", + "CPP": "CPP", + "extend": "拓展和进阶", + "fun": "娱乐", + "API-MJCF": "API-MJCF", + "utils": "工具", + "首页": "首页", + } + return titles.get(section, section) + + +def section_order(section: str) -> tuple[int, str]: + order = { + "MJCF": 0, + "Python": 1, + "CPP": 2, + "extend": 3, + "fun": 4, + "API-MJCF": 5, + "utils": 6, + "首页": 98, + } + return order.get(section, 50), section.lower() + + +def iter_markdown_docs() -> Iterable[Path]: + for root_doc in ROOT_DOCS: + path = REPO_ROOT / root_doc + if path.exists(): + yield path + + for path in sorted(REPO_ROOT.rglob("*.md"), key=natural_sort_key): + if any(part.startswith(".") for part in path.relative_to(REPO_ROOT).parts): + continue + if path.relative_to(REPO_ROOT).as_posix() in ROOT_DOCS: + continue + if path.name in DOC_FILENAMES or path.name.lower() in DOC_FILENAMES: + yield path + + +def natural_sort_key(path: Path) -> list[str | int]: + relative = path.relative_to(REPO_ROOT).as_posix().lower() + return [int(part) if part.isdigit() else part for part in NATURAL_SORT_RE.split(relative)] + + +def build_docs() -> list[DocItem]: + docs: list[DocItem] = [] + seen: set[str] = set() + for path in iter_markdown_docs(): + relative = path.relative_to(REPO_ROOT).as_posix() + if relative in seen: + continue + seen.add(relative) + docs.append( + DocItem( + title=title_from_markdown(path), + path=relative, + section=section_for(path), + url=f"/docs/{web_path(relative)}", + ) + ) + return docs + + +def grouped_docs(docs: list[DocItem], active_path: str) -> list[NavGroup]: + groups: dict[str, list[DocItem]] = {} + for doc in docs: + groups.setdefault(doc.section, []).append(doc) + + nav_groups: list[NavGroup] = [] + for section in sorted(groups, key=section_order): + if section == "首页": + continue + items = groups[section] + nav_groups.append( + NavGroup( + key=section, + title=section_title(section), + items=items, + is_open=any(item.path == active_path for item in items), + ) + ) + return nav_groups + + +def home_docs(docs: list[DocItem]) -> list[DocItem]: + return [doc for doc in docs if doc.section == "首页"] + + +def is_external_url(url: str) -> bool: + parsed = urlsplit(url) + return bool(parsed.scheme or parsed.netloc) or url.startswith(("#", "mailto:", "tel:", "data:")) + + +def resolve_target(base_doc: Path, raw_target: str) -> str: + target = raw_target.strip() + if is_external_url(target): + return target + + parsed = urlsplit(target) + if not parsed.path: + return target + + resolved = (base_doc.parent / unquote(parsed.path)).resolve() + try: + relative = resolved.relative_to(REPO_ROOT).as_posix() + except ValueError: + return target + + suffix = resolved.suffix.lower() + if suffix == ".md": + path = f"/docs/{web_path(relative)}" + else: + path = f"/files/{web_path(relative)}" + + return urlunsplit(("", "", path, parsed.query, parsed.fragment)) + + +def rewrite_markdown_links(markdown_text: str, base_doc: Path) -> str: + def replace_markdown_link(match: re.Match[str]) -> str: + prefix, target, suffix = match.groups() + return f"{prefix}{resolve_target(base_doc, target)}{suffix}" + + return LINK_RE.sub(replace_markdown_link, markdown_text) + + +def rewrite_html_links(html: str, base_doc: Path) -> str: + def replace_html_link(match: re.Match[str]) -> str: + prefix, target, suffix = match.groups() + return f"{prefix}{resolve_target(base_doc, target)}{suffix}" + + return HTML_LINK_RE.sub(replace_html_link, html) + + +def render_markdown(path: Path) -> str: + source = path.read_text(encoding="utf-8") + source = rewrite_markdown_links(source, path) + html = markdown.markdown( + source, + extensions=MARKDOWN_EXTENSIONS, + extension_configs={ + "codehilite": {"guess_lang": False}, + "pymdownx.arithmatex": {"generic": True}, + }, + output_format="html5", + ) + return rewrite_html_links(html, path) + + +def render_doc(request: Request, relative_path: str) -> HTMLResponse: + path = safe_repo_path(relative_path) + if not path.exists() or not path.is_file() or path.suffix.lower() != ".md": + raise HTTPException(status_code=404, detail="Document not found") + + docs = build_docs() + active_path = path.relative_to(REPO_ROOT).as_posix() + return templates.TemplateResponse( + request, + "doc.html", + context={ + "request": request, + "title": title_from_markdown(path), + "content": render_markdown(path), + "docs": docs, + "home_docs": home_docs(docs), + "groups": grouped_docs(docs, active_path), + "active_path": active_path, + }, + ) + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request) -> HTMLResponse: + return render_doc(request, "README.md") + + +@app.get("/docs/{relative_path:path}", response_class=HTMLResponse) +async def docs(request: Request, relative_path: str) -> HTMLResponse: + return render_doc(request, relative_path) + + +@app.get("/files/{relative_path:path}") +async def files(relative_path: str) -> FileResponse: + path = safe_repo_path(relative_path) + if not path.exists() or not path.is_file(): + raise HTTPException(status_code=404, detail="File not found") + return FileResponse(path) diff --git a/mujoco_learning_doc/static/app.js b/mujoco_learning_doc/static/app.js new file mode 100644 index 0000000..91de66b --- /dev/null +++ b/mujoco_learning_doc/static/app.js @@ -0,0 +1,516 @@ +const searchInput = document.querySelector("#doc-search"); +const sidebar = document.querySelector(".sidebar"); +const links = Array.from(document.querySelectorAll(".doc-link")); +const groups = Array.from(document.querySelectorAll(".doc-group")); +const initialOpenGroups = new WeakMap(); +const sidebarScrollKey = "mujoco-learning-doc-sidebar-scroll"; +const groupStateKey = "mujoco-learning-doc-group-state"; + +function loadGroupState() { + try { + return JSON.parse(sessionStorage.getItem(groupStateKey) || "{}"); + } catch { + return {}; + } +} + +function saveGroupState() { + const state = {}; + groups.forEach((group) => { + const title = group.querySelector("summary span")?.textContent?.trim(); + if (title) { + state[title] = group.open; + } + }); + sessionStorage.setItem(groupStateKey, JSON.stringify(state)); +} + +function saveSidebarScroll() { + if (sidebar) { + sessionStorage.setItem(sidebarScrollKey, String(sidebar.scrollTop)); + } +} + +function restoreSidebarScroll() { + if (!sidebar) { + return; + } + const saved = Number(sessionStorage.getItem(sidebarScrollKey)); + if (Number.isFinite(saved)) { + sidebar.scrollTop = saved; + requestAnimationFrame(() => { + sidebar.scrollTop = saved; + }); + } +} + +const savedGroupState = loadGroupState(); +groups.forEach((group) => { + const title = group.querySelector("summary span")?.textContent?.trim(); + if (title && Object.prototype.hasOwnProperty.call(savedGroupState, title)) { + group.open = savedGroupState[title]; + } + initialOpenGroups.set(group, group.open); + group.addEventListener("toggle", () => { + saveGroupState(); + saveSidebarScroll(); + }); +}); + +if (sidebar) { + sidebar.addEventListener("scroll", saveSidebarScroll, { passive: true }); + restoreSidebarScroll(); +} + +links.forEach((link) => { + link.addEventListener("click", () => { + saveGroupState(); + saveSidebarScroll(); + }); +}); + +if (searchInput) { + searchInput.addEventListener("input", () => { + const query = searchInput.value.trim().toLowerCase(); + + links.forEach((link) => { + const haystack = `${link.dataset.title || ""} ${link.dataset.path || ""}`; + link.hidden = query.length > 0 && !haystack.includes(query); + }); + + groups.forEach((group) => { + const visibleLinks = Array.from(group.querySelectorAll(".doc-link")).some( + (link) => !link.hidden, + ); + group.hidden = !visibleLinks; + if (query.length > 0 && visibleLinks) { + group.open = true; + } + if (query.length === 0) { + group.open = initialOpenGroups.get(group); + } + }); + }); +} + +window.addEventListener("beforeunload", () => { + saveGroupState(); + saveSidebarScroll(); +}); + +// --- Interactive Solver Visualizer --- +document.addEventListener("DOMContentLoaded", () => { + const container = document.getElementById("solver-visualizer-container"); + if (!container) return; + + // Dynamically inject the visualizer HTML markup to keep the Markdown file pristine + container.innerHTML = ` + + MuJoCo 接触解算参数实时可视化工具 + + + + + d(r) - 实线:阻抗 + k(r) - 疏虚线:刚度 + b(r) - 密虚线:阻尼 + + + + + 预设模式 (Presets): + + 自定义 (Custom) + 橡胶球 (Rubber Ball) + 牛顿摆/金属 (Newton Cradle) + 缓冲垫 (Cushioning) + + + + Y 轴缩放模式 (Y-axis Scale): + + 绝对物理值 (高度随参数发生变化) + 形状归一化 (显示变化趋势) + + + + + solimp 阻抗曲线 + solref 回弹参考 + + + + + + dmin (d₀): 0.9 + + + + dmax (dwidth): 0.95 + + + + width: 0.001 + + + + midpoint: 0.5 + + + + power: 2 + + + + + + + + 参考格式 (Format): + + 标准 (timeconst, dampratio) + 直接 (stiffness, damping) + + + + + timeconst (τ): 0.02s + + + + dampratio (ζ): 1.0 + + + + + + stiffness (k): 1000 + + + + damping (b): 10 + + + + + + + + `; + + const canvas = document.getElementById("solver-canvas"); + const ctx = canvas.getContext("2d"); + + // Setup High DPI Canvas + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + ctx.scale(dpr, dpr); + + // Inputs + const d0Input = document.getElementById("param-d0"); + const dwidthInput = document.getElementById("param-dwidth"); + const widthInput = document.getElementById("param-width"); + const midpointInput = document.getElementById("param-midpoint"); + const powerInput = document.getElementById("param-power"); + const presetSelect = document.getElementById("preset-select"); + const scaleModeSelect = document.getElementById("scale-mode-select"); + const formatSelect = document.getElementById("ref-format-select"); + + const timeconstInput = document.getElementById("param-timeconst"); + const dampratioInput = document.getElementById("param-dampratio"); + const stiffnessInput = document.getElementById("param-stiffness"); + const dampingInput = document.getElementById("param-damping"); + + // Value displays + const d0Val = document.getElementById("val-d0"); + const dwidthVal = document.getElementById("val-dwidth"); + const widthVal = document.getElementById("val-width"); + const midpointVal = document.getElementById("val-midpoint"); + const powerVal = document.getElementById("val-power"); + const timeconstVal = document.getElementById("val-timeconst"); + const dampratioVal = document.getElementById("val-dampratio"); + const stiffnessVal = document.getElementById("val-stiffness"); + const dampingVal = document.getElementById("val-damping"); + + const standardInputs = document.getElementById("standard-ref-inputs"); + const directInputs = document.getElementById("direct-ref-inputs"); + + // Tab switching + window.switchTab = (tab) => { + document.querySelectorAll(".tab-btn").forEach(btn => btn.classList.remove("active")); + document.querySelectorAll(".tab-content").forEach(content => content.classList.remove("active")); + + if (tab === 'imp') { + document.querySelector(".tab-btn[onclick*='imp']").classList.add("active"); + document.getElementById("imp-controls").classList.add("active"); + } else { + document.querySelector(".tab-btn[onclick*='ref']").classList.add("active"); + document.getElementById("ref-controls").classList.add("active"); + } + }; + + // Presets mapping + const presets = { + rubber: { d0: 0.5, dwidth: 0.95, width: 0.005, midpoint: 0.1, power: 2.0, format: "standard", timeconst: 0.05, dampratio: 0.8 }, + metal: { d0: 0.0, dwidth: 0.6, width: 0.0002, midpoint: 0.5, power: 4.0, format: "standard", timeconst: 0.002, dampratio: 1.0 }, + cushion: { d0: 0.8, dwidth: 0.99, width: 0.008, midpoint: 0.3, power: 2.0, format: "standard", timeconst: 0.04, dampratio: 2.0 } + }; + + presetSelect.addEventListener("change", () => { + const val = presetSelect.value; + if (val === "custom") return; + const p = presets[val]; + + d0Input.value = p.d0; + dwidthInput.value = p.dwidth; + widthInput.value = p.width; + midpointInput.value = p.midpoint; + powerInput.value = p.power; + + formatSelect.value = p.format; + if (p.format === "standard") { + timeconstInput.value = p.timeconst; + dampratioInput.value = p.dampratio; + standardInputs.style.display = "flex"; + directInputs.style.display = "none"; + } else { + stiffnessInput.value = p.stiffness; + dampingInput.value = p.damping; + standardInputs.style.display = "none"; + directInputs.style.display = "flex"; + } + update(); + }); + + scaleModeSelect.addEventListener("change", () => { + update(); + }); + + formatSelect.addEventListener("change", () => { + if (formatSelect.value === "standard") { + standardInputs.style.display = "flex"; + directInputs.style.display = "none"; + } else { + standardInputs.style.display = "none"; + directInputs.style.display = "flex"; + } + presetSelect.value = "custom"; + update(); + }); + + const inputs = [d0Input, dwidthInput, widthInput, midpointInput, powerInput, timeconstInput, dampratioInput, stiffnessInput, dampingInput]; + inputs.forEach(input => { + input.addEventListener("input", () => { + presetSelect.value = "custom"; + update(); + }); + }); + + function getImpedanceValue(r, d0, dwidth, width, midpoint, power) { + if (d0 === dwidth || width <= 1e-6) { + return 0.5 * (d0 + dwidth); + } + const x = Math.min(1.0, Math.max(0.0, Math.abs(r) / width)); + let y = 0; + if (power === 1) { + y = x; + } else if (x <= midpoint) { + const a = 1 / Math.pow(midpoint, power - 1); + y = a * Math.pow(x, power); + } else { + const b = 1 / Math.pow(1 - midpoint, power - 1); + y = 1 - b * Math.pow(1 - x, power); + } + return d0 + y * (dwidth - d0); + } + + function update() { + // Read values + const d0 = parseFloat(d0Input.value); + const dwidth = parseFloat(dwidthInput.value); + const width = parseFloat(widthInput.value); + const midpoint = parseFloat(midpointInput.value); + const power = parseFloat(powerInput.value); + + const isStandard = formatSelect.value === "standard"; + const timeconst = parseFloat(timeconstInput.value); + const dampratio = parseFloat(dampratioInput.value); + const stiffness = parseFloat(stiffnessInput.value); + const damping = parseFloat(dampingInput.value); + const scaleMode = scaleModeSelect ? scaleModeSelect.value : "absolute"; + + // Update value labels + d0Val.textContent = d0.toFixed(2); + dwidthVal.textContent = dwidth.toFixed(2); + widthVal.textContent = width.toFixed(4); + midpointVal.textContent = midpoint.toFixed(2); + powerVal.textContent = power.toFixed(1); + + timeconstVal.textContent = timeconst.toFixed(3); + dampratioVal.textContent = dampratio.toFixed(1); + stiffnessVal.textContent = stiffness; + dampingVal.textContent = damping; + + // Calculate Base K and B + let K_base = 0; + let B_base = 0; + if (isStandard) { + K_base = 1 / (dwidth * dwidth * timeconst * timeconst * dampratio * dampratio); + B_base = 2 / (dwidth * timeconst); + } else { + K_base = stiffness / (dwidth * dwidth); + B_base = damping / dwidth; + } + + // Clear Canvas + const w = rect.width; + const h = rect.height; + ctx.clearRect(0, 0, w, h); + + // Draw grid and axes + ctx.strokeStyle = "#e5e7eb"; + ctx.lineWidth = 1; + ctx.font = "11px Inter, sans-serif"; + ctx.fillStyle = "#9ca3af"; + + const padding = { left: 65, right: 50, top: 40, bottom: 40 }; + const graphW = w - padding.left - padding.right; + const graphH = h - padding.top - padding.bottom; + + // Draw horizontal grid lines (Y-axis grid) + ctx.textAlign = "right"; + for (let i = 0; i <= 4; i++) { + const yVal = i / 4; + const py = padding.top + graphH * (1 - yVal); + ctx.beginPath(); + ctx.moveTo(padding.left, py); + ctx.lineTo(w - padding.right, py); + ctx.stroke(); + if (scaleMode === "normalized") { + ctx.fillText(yVal.toFixed(2), padding.left - 8, py + 4); + } else { + ctx.fillText((yVal * 100).toFixed(0) + "%", padding.left - 8, py + 4); + } + } + + // Draw vertical grid lines (X-axis grid) + ctx.textAlign = "center"; + const maxX = width * 1.5; + for (let i = 0; i <= 3; i++) { + const xVal = (i / 3) * maxX; + const px_fill = padding.left + graphW * (i / 3); + ctx.beginPath(); + ctx.moveTo(px_fill, padding.top); + ctx.lineTo(px_fill, h - padding.bottom); + ctx.stroke(); + ctx.fillText(xVal.toFixed(4), px_fill, h - padding.bottom + 16); + } + + // Labels + ctx.textAlign = "center"; + ctx.fillStyle = "#374151"; + ctx.fillText("渗透深度 r (m)", padding.left + graphW / 2, h - 10); + + // Y-Axis titles + ctx.save(); + ctx.translate(18, padding.top + graphH / 2); + ctx.rotate(-Math.PI / 2); + ctx.fillStyle = "#0f766e"; + ctx.textAlign = "center"; + if (scaleMode === "normalized") { + ctx.fillText("阻抗 / 归一化强度", 0, 0); + } else { + ctx.fillText("阻抗及物理值占比 (%)", 0, 0); + } + ctx.restore(); + ctx.textAlign = "left"; // Reset to default + + // Draw curves + const points = 100; + const dPoints = []; + const kPoints = []; + const bPoints = []; + + let maxK = 0; + let maxB = 0; + + for (let i = 0; i <= points; i++) { + const r = (i / points) * maxX; + const d = getImpedanceValue(r, d0, dwidth, width, midpoint, power); + const k = d * K_base; + const b = d * B_base; + + dPoints.push({ r, val: d }); + kPoints.push({ r, val: k }); + bPoints.push({ r, val: b }); + + if (k > maxK) maxK = k; + if (b > maxB) maxB = b; + } + + // Display peak stiffness and damping text dynamically on canvas to reflect solref changes + ctx.font = "bold 11px Inter, sans-serif"; + ctx.fillStyle = "#ef4444"; + ctx.fillText(`k_max (最大刚度): ${maxK.toFixed(0)} N/m`, padding.left + 10, padding.top - 15); + ctx.fillStyle = "#3b82f6"; + ctx.fillText(`b_max (最大阻尼): ${maxB.toFixed(1)} Ns/m`, padding.left + 220, padding.top - 15); + + // Render d(r) curve + ctx.beginPath(); + ctx.strokeStyle = "#0f766e"; // Teal + ctx.lineWidth = 3; + dPoints.forEach((p, idx) => { + const px = padding.left + (p.r / maxX) * graphW; + const py = padding.top + (1 - p.val) * graphH; + if (idx === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + }); + ctx.stroke(); + + // Render k(r) curve + ctx.beginPath(); + ctx.strokeStyle = "#ef4444"; // Red + ctx.lineWidth = 2; + ctx.setLineDash([4, 4]); + kPoints.forEach((p, idx) => { + const px = padding.left + (p.r / maxX) * graphW; + let normVal = 0; + if (scaleMode === "normalized") { + normVal = maxK > 0 ? p.val / maxK : 0; + } else { + const K_scale = Math.max(10000, maxK); + normVal = p.val / K_scale; + } + const py = padding.top + (1 - normVal) * graphH; + if (idx === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + }); + ctx.stroke(); + + // Render b(r) curve + ctx.beginPath(); + ctx.strokeStyle = "#3b82f6"; // Blue + ctx.lineWidth = 2; + ctx.setLineDash([2, 2]); + bPoints.forEach((p, idx) => { + const px = padding.left + (p.r / maxX) * graphW; + let normVal = 0; + if (scaleMode === "normalized") { + normVal = maxB > 0 ? p.val / maxB : 0; + } else { + const B_scale = Math.max(500, maxB); + normVal = p.val / B_scale; + } + const py = padding.top + (1 - normVal) * graphH; + if (idx === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + }); + ctx.stroke(); + ctx.setLineDash([]); // Reset + } + + // Initial update + update(); +}); diff --git a/mujoco_learning_doc/static/style.css b/mujoco_learning_doc/static/style.css new file mode 100644 index 0000000..847a036 --- /dev/null +++ b/mujoco_learning_doc/static/style.css @@ -0,0 +1,555 @@ +:root { + color-scheme: light; + --bg: #f6f7f9; + --panel: #ffffff; + --text: #1d2430; + --muted: #697386; + --line: #d8dee8; + --accent: #0f766e; + --accent-soft: #e4f4f1; + --code-bg: #101820; + --code-text: #edf5f7; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + "Microsoft YaHei", sans-serif; +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.app-shell { + display: grid; + grid-template-columns: 320px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; + border-right: 1px solid var(--line); + background: var(--panel); + padding: 20px 16px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + color: var(--text); + margin-bottom: 20px; +} + +.brand:hover { + text-decoration: none; +} + +.brand-mark { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border-radius: 8px; + background: var(--accent); + color: #fff; + font-weight: 750; +} + +.brand strong, +.brand small { + display: block; +} + +.brand small { + margin-top: 2px; + color: var(--muted); +} + +.search { + display: block; + margin-bottom: 18px; +} + +.search span { + display: block; + margin-bottom: 7px; + color: var(--muted); + font-size: 13px; +} + +.search input { + width: 100%; + min-height: 38px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 8px 10px; + color: var(--text); + background: #fff; + font: inherit; +} + +.doc-home { + display: grid; + gap: 4px; + border-bottom: 1px solid var(--line); + margin-bottom: 14px; + padding-bottom: 14px; +} + +.doc-link-home span { + font-size: 15px; +} + +.doc-group { + margin: 0 0 10px; + border: 1px solid transparent; + border-radius: 8px; +} + +.doc-group[open] { + border-color: #edf1f5; + background: #fbfcfd; +} + +.doc-group summary { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 42px; + border-radius: 8px; + padding: 9px 10px; + color: var(--text); + cursor: pointer; + list-style: none; + user-select: none; +} + +.doc-group summary::-webkit-details-marker { + display: none; +} + +.doc-group summary::before { + content: "›"; + display: inline-grid; + width: 18px; + height: 18px; + margin-right: 7px; + place-items: center; + color: var(--muted); + font-size: 20px; + line-height: 1; + transform: rotate(0deg); + transition: transform 0.16s ease; +} + +.doc-group[open] summary::before { + transform: rotate(90deg); +} + +.doc-group summary:hover { + background: #eef2f6; +} + +.doc-group summary span { + flex: 1; + font-size: 17px; + font-weight: 760; + overflow-wrap: anywhere; +} + +.doc-group summary small { + min-width: 26px; + border-radius: 999px; + background: #edf1f5; + color: var(--muted); + padding: 2px 7px; + text-align: center; + font-size: 12px; +} + +.doc-group-items { + display: grid; + gap: 2px; + padding: 2px 6px 8px 20px; +} + +.doc-link { + display: block; + padding: 9px 10px; + border-radius: 8px; + color: var(--text); +} + +.doc-link:hover { + background: #eef2f6; + text-decoration: none; +} + +.doc-link.is-active { + background: var(--accent-soft); + color: #075e56; +} + +.doc-link span, +.doc-link small { + display: block; + overflow-wrap: anywhere; +} + +.doc-link span { + font-size: 14px; + font-weight: 650; +} + +.doc-link small { + margin-top: 3px; + color: var(--muted); + font-size: 11px; + line-height: 1.35; +} + +.content { + min-width: 0; + padding: 34px clamp(20px, 4vw, 56px); +} + +.markdown-body { + max-width: 980px; + margin: 0 auto; + line-height: 1.75; + font-size: 16px; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4 { + line-height: 1.3; + scroll-margin-top: 18px; +} + +.markdown-body h1 { + margin: 0 0 24px; + font-size: 36px; +} + +.markdown-body h2 { + margin-top: 34px; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); + font-size: 25px; +} + +.markdown-body h3 { + margin-top: 26px; + font-size: 20px; +} + +.markdown-body img { + max-width: 100%; + height: auto; + border-radius: 8px; + border: 1px solid var(--line); + background: #fff; +} + +.markdown-body table { + display: block; + width: 100%; + overflow-x: auto; + border-collapse: collapse; +} + +.markdown-body th, +.markdown-body td { + border: 1px solid var(--line); + padding: 8px 10px; + text-align: left; + vertical-align: top; +} + +.markdown-body th { + background: #edf1f5; +} + +.markdown-body code { + border-radius: 5px; + background: #e8edf2; + padding: 2px 5px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 0.92em; +} + +.markdown-body pre { + overflow-x: auto; + border-radius: 8px; + background: var(--code-bg); + padding: 16px; +} + +.markdown-body pre code, +.markdown-body .codehilite code { + background: transparent; + color: var(--code-text); + padding: 0; +} + +.codehilite { + overflow-x: auto; + border-radius: 8px; + background: var(--code-bg); +} + +.codehilite pre { + margin: 0; +} + +.markdown-body blockquote { + margin: 18px 0; + border-left: 4px solid var(--accent); + padding: 6px 0 6px 16px; + color: #344052; + background: #eef7f5; +} + +.markdown-body mjx-container[jax="CHTML"][display="true"] { + overflow-x: auto; + overflow-y: hidden; + max-width: 100%; + padding: 8px 0; +} + +.markdown-body mjx-container { + line-height: 1.35; +} + +@media (max-width: 860px) { + .app-shell { + display: block; + } + + .sidebar { + position: static; + height: auto; + max-height: 52vh; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .content { + padding: 24px 16px; + } + + .markdown-body h1 { + font-size: 30px; + } +} + +/* Interactive Solver Parameter Visualizer Styling */ +.solver-visualizer-card { + margin: 32px 0; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04); + padding: 24px; + font-family: inherit; +} + +.solver-visualizer-card h3 { + margin-top: 0; + margin-bottom: 20px; + color: var(--text); + font-size: 1.25rem; + font-weight: 700; + border-bottom: 2px solid var(--accent-soft); + padding-bottom: 12px; +} + +.visualizer-layout { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 24px; +} + +@media (max-width: 768px) { + .visualizer-layout { + grid-template-columns: 1fr; + } +} + +.canvas-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #fbfcfd; + border: 1px solid var(--line); + border-radius: 8px; + padding: 16px; +} + +.canvas-panel canvas { + max-width: 100%; + height: auto; + background: transparent; +} + +.plot-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 12px; + font-size: 13px; + color: var(--muted); +} + +.legend-item { + display: flex; + align-items: center; + gap: 6px; +} + +.color-box { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 3px; +} + +.color-box.d-curve { background: #0f766e; } /* Teal */ +.color-box.k-curve { background: #ef4444; } /* Red */ +.color-box.b-curve { background: #3b82f6; } /* Blue */ + +.controls-panel { + display: flex; + flex-direction: column; + gap: 16px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.control-group label { + font-size: 14px; + font-weight: 600; + color: var(--text); +} + +.control-group select { + padding: 8px 12px; + border: 1px solid var(--line); + border-radius: 6px; + background: #fff; + color: var(--text); + font-size: 14px; + outline: none; + cursor: pointer; + transition: border-color 0.2s; +} + +.control-group select:focus { + border-color: var(--accent); +} + +.tab-container { + display: flex; + border-bottom: 1px solid var(--line); + margin-bottom: 8px; +} + +.tab-btn { + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 8px 16px; + font-size: 14px; + font-weight: 600; + color: var(--muted); + cursor: pointer; + transition: all 0.2s; +} + +.tab-btn:hover { + color: var(--text); +} + +.tab-btn.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.tab-content { + display: none; + flex-direction: column; + gap: 14px; +} + +.tab-content.active { + display: flex; +} + +.slider-item { + display: flex; + flex-direction: column; + gap: 6px; +} + +.slider-item label { + display: flex; + justify-content: space-between; + font-size: 13px; + font-weight: 500; + color: var(--text); +} + +.slider-item label span { + font-family: monospace; + font-weight: 700; + color: var(--accent); +} + +.slider-item input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--line); + outline: none; +} + +.slider-item input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + box-shadow: 0 1px 4px rgba(0,0,0,0.2); + transition: transform 0.1s; +} + +.slider-item input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); +} + diff --git a/mujoco_learning_doc/templates/doc.html b/mujoco_learning_doc/templates/doc.html new file mode 100644 index 0000000..b1b9cba --- /dev/null +++ b/mujoco_learning_doc/templates/doc.html @@ -0,0 +1,88 @@ + + + + + + {{ title }} - MuJoCo 教程 + + + + + + + + + {{ content | safe }} + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f0a17d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "mujoco-learning-docs" +version = "0.1.0" +description = "Local FastAPI documentation site and Python MuJoCo tutorial environment." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.111", + "uvicorn[standard]>=0.30", + "jinja2>=3.1", + "markdown>=3.6", + "pymdown-extensions>=10.8", + "pygments>=2.18", + "mujoco>=3.2", +] + +[project.optional-dependencies] +dev = [ + "httpx>=0.27", +] + +[project.scripts] +mujoco_learning_doc = "mujoco_learning_doc.main:run" + +[tool.setuptools] +packages = ["mujoco_learning_doc"] +include-package-data = true + +[tool.setuptools.package-data] +mujoco_learning_doc = ["templates/*.html", "static/*.css", "static/*.js"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0a71b35 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup + + +DEPENDENCIES = [ + "fastapi>=0.111", + "uvicorn[standard]>=0.30", + "jinja2>=3.1", + "markdown>=3.6", + "pymdown-extensions>=10.8", + "pygments>=2.18", + "mujoco>=3.2", +] + + +setup( + name="mujoco-learning-docs", + version="0.1.0", + description="Local FastAPI documentation site and Python MuJoCo tutorial environment.", + packages=["mujoco_learning_doc"], + include_package_data=True, + package_data={"mujoco_learning_doc": ["templates/*.html", "static/*.css", "static/*.js"]}, + python_requires=">=3.10", + install_requires=DEPENDENCIES, + extras_require={"dev": ["httpx>=0.27"]}, + entry_points={"console_scripts": ["mujoco_learning_doc=mujoco_learning_doc.main:run"]}, +) diff --git a/skills/mujoco-cpp-build/SKILL.md b/skills/mujoco-cpp-build/SKILL.md new file mode 100644 index 0000000..3a202e2 --- /dev/null +++ b/skills/mujoco-cpp-build/SKILL.md @@ -0,0 +1,62 @@ +--- +name: mujoco-cpp-build +description: Use when building C++ examples, compiling simulation executables, configuring CMake, or resolving C++ building errors in the MuJoCo learning repository. It requires the agent to actively prompt the user for build requirements (source vs release path, target directories). +--- + +# MuJoCo C++ Build Skill + +Use this skill to automate the building of C++ chapters and simulate tools in the repository. This supports both **Source-built Install** (e.g., `/opt/mujoco`) and **Pre-compiled Release** (custom directory) options. + +## Response Style + +- Keep answers concise and compact. If the active agent is framed as a chatbot, avoid excessive headings, blank lines, and long segmented explanations; prefer dense short paragraphs or a small bullet list. +- When mentioning a dependency, official API, external project, or useful reference, include a clickable link when one is known. +- When referencing this tutorial repository in a chat-only context, prefer GitHub links to project files instead of local absolute paths. Use repository-relative paths only when working inside a local clone. +- If the build/debug task involves ray caster examples, mention the maintained project when relevant: [Albusgive/mujoco_ray_caster](https://github.com/Albusgive/mujoco_ray_caster). + +## CRITICAL: Active Prompting Requirement +Before starting any compilation or editing of `CMakeLists.txt`, **you must actively ask the user** for their compilation requirements. Use a clear, formatted message (or ask_question tool if appropriate) to align on: +1. **MuJoCo Library Choice**: + - Option A: Source-built and installed to standard path `/opt/mujoco` (using `find_package(mujoco REQUIRED PATHS /opt/mujoco/lib/cmake NO_DEFAULT_PATH)`). + - Option B: Pre-compiled Release or Source-built in a custom directory (e.g. `/path/to/mujoco-3.3.1`). +2. **Custom Directory Path** (Only if Option B is chosen): Ask the user to provide the absolute path to their MuJoCo directory. +3. **Target Chapter**: Which chapter or executable to compile (e.g., `CPP/Chapter1-make/basic`, `CPP/Chapter2-view&step`, or "All"). + +## Workflow + +1. **Prompt the User**: Present the build options clearly and wait for their input. +2. **Locate the Workspace**: Resolve the repository path dynamically (using `scripts/find_repo.py`). +3. **Configure CMakeLists.txt**: + - Modify the target chapter's `CMakeLists.txt` based on the user's choices. + - For **Option A** (Installed under `/opt`): + ```cmake + set(MUJOCO_FOLDER /opt/mujoco/lib/cmake) + find_package(mujoco REQUIRED PATHS ${MUJOCO_FOLDER} NO_DEFAULT_PATH) + target_link_libraries(your_target mujoco::mujoco glut GL GLU glfw) + ``` + - For **Option B** (Custom path, e.g., `/path/to/mujoco`): + ```cmake + set(MUJOCO_PATH "/path/to/mujoco") + include_directories(${MUJOCO_PATH}/include) + # For Source-built custom paths: + link_directories(${MUJOCO_PATH}/build/bin) + set(MUJOCO_LIB ${MUJOCO_PATH}/build/lib/libmujoco.so) + # For Release pre-compiled paths: + # link_directories(${MUJOCO_PATH}/bin) + # set(MUJOCO_LIB ${MUJOCO_PATH}/lib/libmujoco.so) + + target_link_libraries(your_target ${MUJOCO_LIB} glut GL GLU glfw) + ``` +4. **Compile the Executable**: + - Create a `build` directory under the target chapter (e.g. `CPP/Chapter1-make/build/`). + - Run `cmake ..` and `make` (or `ninja`). +5. **Handle Dependency Errors**: + - If missing `GLFW` or `GL` headers: inform the user and suggest running `sudo apt-get install libglfw3-dev libgl1-mesa-dev libglu1-mesa-dev freeglut3-dev`. + +## Validation + +After compilation, verify by running the binary with a sample model if available: +```bash +./basic ../../../API-MJCF/pointer.xml +``` +Verify stdout returns no errors and simulation runs. diff --git a/skills/mujoco-cpp-build/scripts/find_repo.py b/skills/mujoco-cpp-build/scripts/find_repo.py new file mode 100644 index 0000000..b33f200 --- /dev/null +++ b/skills/mujoco-cpp-build/scripts/find_repo.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +from pathlib import Path + + +REQUIRED = ("directory.md", "MJCF", "Python", "CPP") +SKILL_ROOT = Path(__file__).resolve().parents[1] +LOCAL_CONFIG = SKILL_ROOT / ".local" / "repo_path.txt" + + +def is_repo_root(path: Path) -> bool: + return all((path / item).exists() for item in REQUIRED) + + +def ancestors(path: Path): + current = path.resolve() + yield current + yield from current.parents + + +def saved_repo_path() -> Path | None: + if not LOCAL_CONFIG.exists(): + return None + value = LOCAL_CONFIG.read_text(encoding="utf-8").strip() + if not value: + return None + return Path(value).expanduser() + + +def candidates() -> list[Path]: + items: list[Path] = [] + saved_root = saved_repo_path() + if saved_root: + items.append(saved_root) + + env_root = os.environ.get("MUJOCO_LEARNING_ROOT") + if env_root: + items.append(Path(env_root).expanduser()) + + for base in ancestors(Path.cwd()): + items.append(base) + + script_path = Path(__file__).resolve() + items.extend(script_path.parents) + + home = Path.home() + items.extend( + [ + home / "mujoco_learning", + home / "mujoco-learning", + home / "code" / "mujoco_learning", + home / "projects" / "mujoco_learning", + home / "workspace" / "mujoco_learning", + ] + ) + return items + + +def save_repo_path(path: Path) -> int: + root = path.expanduser().resolve() + if not is_repo_root(root): + print(f"Invalid MuJoCo tutorial repository path: {root}") + print("Expected directory.md plus MJCF/, Python/, and CPP/ under that path.") + return 1 + LOCAL_CONFIG.parent.mkdir(parents=True, exist_ok=True) + LOCAL_CONFIG.write_text(str(root), encoding="utf-8") + print(root) + return 0 + + +def clear_repo_path() -> int: + if LOCAL_CONFIG.exists(): + LOCAL_CONFIG.unlink() + print("Cleared saved MuJoCo tutorial repository path.") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Find or configure the local MuJoCo tutorial repository.") + parser.add_argument("--set", dest="set_path", help="Save the local MuJoCo tutorial repository path.") + parser.add_argument("--clear", action="store_true", help="Clear the saved repository path.") + args = parser.parse_args() + + if args.clear: + return clear_repo_path() + if args.set_path: + return save_repo_path(Path(args.set_path)) + + for item in candidates(): + if is_repo_root(item): + print(item.resolve()) + return 0 + + if LOCAL_CONFIG.exists(): + print("Saved MuJoCo tutorial repository path is invalid or unavailable. Ask the user for the new clone path and run this script with --set PATH.") + return 1 + + print( + "MuJoCo tutorial repository not found. Ask the user for the local clone path and run this script with --set PATH.", + end="", + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/mujoco-engineering/SKILL.md b/skills/mujoco-engineering/SKILL.md new file mode 100644 index 0000000..f5fec28 --- /dev/null +++ b/skills/mujoco-engineering/SKILL.md @@ -0,0 +1,59 @@ +--- +name: mujoco-engineering +description: Use when an agent needs to implement, reproduce, debug, or extend MuJoCo projects using this repository's MJCF, Python, C++, ray casting, soft contact, sensors, rendering, viewer, force, or build examples. Use it to locate relevant tutorial code paths before writing or modifying MuJoCo code. +--- + +# MuJoCo Engineering + +Use this skill for engineering work: reproduce examples, build MuJoCo scenes, write Python/C++ API code, debug MJCF, add sensors, implement ray casting, tune contact, or adapt this tutorial repository into working code. + +## Repository Path Policy + +Never hard-code a developer's local absolute path. Resolve the repository dynamically: + +- Prefer the current workspace if it contains `directory.md`, `MJCF/`, `Python/`, and `CPP/`. +- If this skill lives under `skills/mujoco-engineering` inside a clone, use the parent repository root. +- If installed globally, run `scripts/find_repo.py` from this skill. It checks the skill-local saved path, current workspace, `MUJOCO_LEARNING_ROOT`, and common clone directory names. +- On first use, if `scripts/find_repo.py` cannot find the repository, ask the user for the local clone path and save it with `scripts/find_repo.py --set PATH`. +- If the saved path becomes invalid or unavailable, ask again and rerun `scripts/find_repo.py --set PATH`. +- If no local clone is available, use the clone URL before referencing local files. + +The saved path lives in `.local/repo_path.txt` under the installed skill directory. This is a cross-platform local config file; do not edit `SKILL.md` to store user paths. + +All reference paths below are repository-relative and safe to show in public docs. + +## Engineering Workflow + +1. Classify the task: MJCF authoring, Python API, C++ API, build/config, sensor/rendering, ray casting, contact/solver tuning, or extension. +2. Check `references/quick-reference.md` first for core API usage, syntax, formulas, and official documentation deep links. +3. Read `references/tutorial-map.md` and select the closest tutorial and code example if more details are needed. +4. Inspect the referenced source files before implementing. +5. Reuse repository patterns and asset paths rather than inventing new structure. +6. If engineering work repeatedly fails because of parameter uncertainty, unstable behavior, or unclear validation results, selectively build a minimal test demo from the repository's test models, XML parameters, and chapter examples to isolate the issue. +7. Choose validation based on the agent/model capability: + - If the agent can understand images or video, it may render screenshots or record a short demo from the simulation, then compare the demo result with the expected behavior and with the real development output. It can also read numeric state directly from simulation when that is more precise. + - If the agent is text-only, it must validate through custom simulation outputs: print, log, or assert positions, velocities, contacts, sensor values, forces, ray distances, rendered buffer metadata, or other task-specific data. +8. Validate with the smallest relevant command first: + - Python examples: run the chapter script in the target Python/conda environment. + - C++ examples: inspect `CMakeLists.txt`, then build in that chapter's build directory (or use `mujoco-cpp-build` skill). + - Docs site changes: run `python -m compileall mujoco_learning_doc` and verify affected routes. +9. If a MuJoCo API detail is uncertain, consult `references/quick-reference.md` or official docs after checking the tutorial. + +## Implementation Guidance + +- Keep responses concise and compact. If the active agent is framed as a chatbot, avoid excessive headings, blank lines, and long segmented explanations; prefer dense short paragraphs or a small bullet list. +- When mentioning a dependency, official API, external project, or useful reference, include a clickable link when one is known. +- When referencing this tutorial repository in a chat-only context, prefer GitHub links to project files instead of local absolute paths. Use repository-relative paths only when working inside a local clone. +- For ray caster / depth camera / ray-based ranging work, recommend the maintained project when relevant: [Albusgive/mujoco_ray_caster](https://github.com/Albusgive/mujoco_ray_caster). +- Check `references/quick-reference.md` for fast, copy-pasteable implementation templates. +- Keep MJCF paths relative to the scene XML or repository root, matching local examples. +- Separate model-side configuration from runtime code: MJCF defines bodies/geoms/joints/sensors/actuators; Python/C++ loads the model, creates data, steps, reads/writes arrays, and renders. +- For ray or ray caster work, start from `Python/Chapter7-ray/`, `CPP/Chapter8-ray/`, and `extend/deep_camera/`; for maintained ray caster code, use `https://github.com/Albusgive/mujoco_ray_caster`. +- For soft contact, start from `extend/soft_contact/tutorial.md` and its XML/Python/C++ examples before tuning `solimp`, `solref`, stiffness, damping, or contact dimensions. +- For build problems, prefer the local chapter `CMakeLists.txt` patterns and then official MuJoCo build documentation. +- Prefer measurable validation over visual inspection when exact behavior matters; use visual screenshots/video as an optional diagnostic aid, not the only proof, unless the task is specifically visual. + +## Reference Map + +- Quick cheatsheet, APIs, formulas, and deep links: `references/quick-reference.md` +- Detailed file mapping: `references/tutorial-map.md` diff --git a/skills/mujoco-engineering/agents/openai.yaml b/skills/mujoco-engineering/agents/openai.yaml new file mode 100644 index 0000000..7d17d65 --- /dev/null +++ b/skills/mujoco-engineering/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "MuJoCo Engineering" + short_description: "Build MuJoCo scenes and API code from examples" + brand_color: "#0F766E" + default_prompt: "Use $mujoco-engineering to implement a MuJoCo sensor example from the repository patterns." +policy: + allow_implicit_invocation: true diff --git a/skills/mujoco-engineering/references/quick-reference.md b/skills/mujoco-engineering/references/quick-reference.md new file mode 100644 index 0000000..e526d77 --- /dev/null +++ b/skills/mujoco-engineering/references/quick-reference.md @@ -0,0 +1,374 @@ +# MuJoCo Quick Reference & Essence (速查手册与精华提炼) + +This document contains key formulas, API usage patterns, configuration references, and official documentation deep links. Use this for quick lookup during coding and explanation. + +--- + +## 1. MJCF Modeling Quick Reference (建模速查) + +### 1.1 Soft Contact (软接触与求解器) +MuJoCo simulates soft contacts using a dynamic "spring-damper" model. +* **Equation**: $a_{ref} = -b v - k r$ (where $r$ is penetration depth, $v$ is velocity, $b$ is damping, $k$ is stiffness). +* **solimp**: Calculates the constraint impedance $d(r) \in (0, 1)$. + * Attributes: `(d0, dwidth, width, midpoint, power)` + * Default: `(0.9, 0.95, 0.001, 0.5, 2)` + * XML Syntax: `` + * Official Reference: [MuJoCo option-solimp](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-solimp) | [Computation Contacts](https://mujoco.readthedocs.io/en/latest/computation/index.html#contacts) +* **solref**: Calculates stiffness $k$ and damping $b$. + * Positive values: `(timeconst, dampratio)`. $b = \frac{2}{d_{width} \cdot timeconst}$, $k = \frac{d(r)}{d_{width} \cdot timeconst^2 \cdot dampratio^2}$. + * Negative values: `(-stiffness, -damping)`. $b = \frac{damping}{d_{width}}$, $k = \frac{stiffness \cdot d(r)}{d_{width}^2}$. + * XML Syntax: `` or `` + * Official Reference: [MuJoCo option-solref](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-solref) +* **Tuning Rules**: + * For rubber/bouncy materials: set a larger `width` (e.g., 0.05) and adjust `solref` to control elasticity. + * To prevent interpenetration (穿模): increase stiffness $k$ (by decreasing `timeconst` or increasing `-stiffness`). If it jitters, increase damping $b$. + +### 1.2 Joints, Actuators & Sensors +* **Joints**: `` + * Types: `hinge` (旋转), `slide` (滑动), `ball` (球形/三自由度), `free` (自由度/六自由度). + * Official Reference: [MuJoCo joint](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint) +* **Actuators**: + * Torque control: `` + * Position control: `` + * Velocity control: `` + * Official Reference: [MuJoCo actuator](https://mujoco.readthedocs.io/en/latest/XMLreference.html#actuator) +* **Sensors**: + * IMU / Gyro / Accel: ``, `` + * Touch sensor: `` + * Force / Torque: ``, `` + * Official Reference: [MuJoCo sensor](https://mujoco.readthedocs.io/en/latest/XMLreference.html#sensor) + +--- + +## 2. Python API Quick Reference (Python API) + +### 2.1 Model Loading & Stepping (加载与步进) +```python +import mujoco + +# Load model and create runtime data +model = mujoco.MjModel.from_xml_path("scene.xml") +data = mujoco.MjData(model) + +# Simulation step +mujoco.mj_step(model, data) +``` +* Official Reference: [MuJoCo Python Programming](https://mujoco.readthedocs.io/en/latest/programming.html#initialization) + +### 2.2 Object Lookup (对象查名/ID) +```python +# Convert name to ID +geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "geom_name") +if geom_id == -1: + raise ValueError("Geom not found") + +# Get name from ID +geom_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_GEOM, geom_id) +``` +* Official Reference: [Indices and Names](https://mujoco.readthedocs.io/en/latest/programming.html#indices-and-names) + +### 2.3 Sensor Reading (读取传感器) +* **New and Preferred Way (Attribute Access)**: + ```python + sensor_val = data.sensor("sensor_name").data + ``` +* **Classic Way (Index Access)**: + ```python + sensor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, "sensor_name") + start_idx = model.sensor_adr[sensor_id] + dim = model.sensor_dim[sensor_id] + sensor_val = data.sensordata[start_idx : start_idx + dim] + ``` +* Official Reference: [MuJoCo Sensor Programming](https://mujoco.readthedocs.io/en/latest/programming.html#sensors) + +### 2.4 Offscreen Rendering & Camera Pixels (相机画面读取) +```python +import glfw +import numpy as np + +# Initialize GLFW and hidden window +glfw.init() +glfw.window_hint(glfw.VISIBLE, glfw.FALSE) +window = glfw.create_window(640, 480, "offscreen", None, None) +glfw.make_context_current(window) + +# Setup renderer and camera +camera = mujoco.MjvCamera() +camera.type = mujoco.mjtCamera.mjCAMERA_FIXED +camera.fixedcamid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "my_camera") + +scene = mujoco.MjvScene(model, maxgeom=1000) +context = mujoco.MjrContext(model, mujoco.mjtFontScale.mjFONTSCALE_150) +mujoco.mjr_setBuffer(mujoco.mjtFramebuffer.mjFB_OFFSCREEN, context) + +# Render & read pixels +viewport = mujoco.MjrRect(0, 0, 640, 480) +mujoco.mjv_updateScene(model, data, mujoco.MjvOption(), None, camera, mujoco.mjtCatBit.mjCAT_ALL, scene) +mujoco.mjr_render(viewport, scene, context) + +rgb = np.zeros((480, 640, 3), dtype=np.uint8) +depth = np.zeros((480, 640), dtype=np.float32) +mujoco.mjr_readPixels(rgb, depth, viewport, context) +# Flip vertically because OpenGL coordinates start from bottom-left +rgb = np.flipud(rgb) +depth = np.flipud(depth) +``` +* Official Reference: [MuJoCo Visualization Programming](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) + +--- + +## 3. C++ API & Compilation Quick Reference (C++ 编译与开发) + +### 3.1 C++ Simulation Loop (基础C++仿真循环) +```cpp +#include +#include + +mjModel* m = nullptr; +mjData* d = nullptr; + +int main() { + char error[1000]; + m = mj_loadXML("scene.xml", nullptr, error, 1000); + if (!m) { + mju_error("Could not load model: %s", error); + } + d = mj_makeData(m); + + // Main step loop + while (!glfwWindowShouldClose(window)) { + mj_step(m, d); + } + + mj_deleteData(d); + mj_deleteModel(m); + return 0; +} +``` + +### 3.2 Minimum CMakeLists.txt (C++ 最小编译模板) +```cmake +cmake_minimum_required(VERSION 3.20) +project(mujoco_cpp_example) + +# Option A: From CMake install directory (e.g., /opt/mujoco) +set(MUJOCO_FOLDER /opt/mujoco/lib/cmake) +find_package(mujoco REQUIRED PATHS ${MUJOCO_FOLDER} NO_DEFAULT_PATH) + +add_executable(basic basic.cc) +target_link_libraries(basic mujoco::mujoco glut GL GLU glfw) +``` +* Official Reference: [Building MuJoCo from source](https://mujoco.readthedocs.io/en/latest/programming.html#building-mujoco-from-source) + +--- + +## 4. Common Pitfalls & Solutions (避坑指南) + +* **GLIBCXX not found in Conda**: + * *Error*: `ImportError: /lib/x86_64-linux-gnu/libstdc++.so.6: version 'GLIBCXX_3.4.30' not found` + * *Fix*: Run `conda install -c conda-forge libstdcxx-ng` inside the activated conda environment. +* **Header Include Order in C++**: + * *Pitfall*: Including OpenGL or GLFW headers *before* MuJoCo headers can sometimes cause macro redefinition errors. + * *Fix*: Always include `` first. +* **GLFW Context in Headless Servers**: + * *Pitfall*: Calling `glfw.create_window` on Linux headless servers without setting `glfw.window_hint(glfw.VISIBLE, glfw.FALSE)` or without X11 server will crash. + * *Fix*: Use `os.environ["DISPLAY"]` checks or use EGL/OSMesa instead of GLFW if graphics driver is absent. + +--- + +## 5. Interactive & Visual Teaching Templates (交互式与仿真式教学模板) + +### 5.1 Standalone HTML Slider Visualizer (HTML 数学曲线交互分析模板) +Generate and save this template as an HTML file in the workspace or artifact directory when teaching curves like `solimp` or spring-damper equations: +```html + + + + + MuJoCo solimp 曲线交互可视化 + + + + + + MuJoCo solimp (阻抗曲线) 交互分析器 + 公式: $d(r) = d_0 + Y(r/width) \cdot (d_{width} - d_0)$,调节下方参数滑块观察阻抗如何随穿模深度 $r$ 动态变化: + + + d0 (起始阻抗) + + 0.9 + + + dwidth (最大阻抗) + + 0.95 + + + width (归一化宽度) + + 0.001 + + + midpoint (分界点) + + 0.5 + + + power (曲线幂指数) + + 2 + + + + + + + + +``` + +### 5.2 Python Passive Viewer Keyboard Interaction (`launch_passive` 键盘交互仿真) +Write this script when teaching users how to interactively test control loops, apply forces, or tune friction dynamically inside MuJoCo's 3D viewer: +```python +import mujoco +import mujoco.viewer +import time +import sys + +# Load model (make sure you have an actuator or control joint) +model = mujoco.MjModel.from_xml_path("API-MJCF/mecanum.xml") +data = mujoco.MjData(model) + +# Active keyboard control loop +with mujoco.viewer.launch_passive(model, data) as viewer: + print("\n=== 交互控制台 ===") + print("按下 [W] 增加控制目标,按下 [S] 减小控制目标") + print("按下 [Q] 退出仿真") + + target_ctrl = 0.0 + + while viewer.is_running(): + step_start = time.time() + + # Check key states (viewer.user_btn or custom key handlers if running window) + # Note: In launch_passive, you can listen to terminal or use window events. + # Alternatively, modify data.ctrl directly based on terminal triggers: + # data.ctrl[0] = target_ctrl + + mujoco.mj_step(model, data) + viewer.sync() + + # Step rate limiter + time_elapsed = time.time() - step_start + if time_elapsed < model.opt.timestep: + time.sleep(model.opt.timestep - time_elapsed) +``` + +### 5.3 Custom 3D Debug Overlays (绘制自定义调试几何形状) +Use this boilerplate to teach users how to draw custom geometric objects (arrows, spheres, coordinate axes) dynamically in the 3D viewer without modifying the XML file: +```python +import mujoco +import mujoco.viewer +import numpy as np +import time + +model = mujoco.MjModel.from_xml_path("API-MJCF/force.xml") +data = mujoco.MjData(model) + +with mujoco.viewer.launch_passive(model, data) as viewer: + while viewer.is_running(): + step_start = time.time() + mujoco.mj_step(model, data) + + # Draw a custom 3D arrow to visualize contact force or direction + # ngeoms should be reset or incremented on user_scn + viewer.user_scn.ngeom = 0 # Clear previous custom shapes + + # Add a custom sphere at coordinates (0, 0, 1) + mujoco.mjv_initGeom( + viewer.user_scn.geoms[viewer.user_scn.ngeom], + type=mujoco.mjtGeom.mjGEOM_SPHERE, + size=[0.05, 0, 0], + pos=[0.0, 0.0, 1.0], + mat=np.eye(3).flatten(), + rgba=[0, 1, 0, 0.8] # Semi-transparent green + ) + viewer.user_scn.ngeom += 1 + + # Add a custom coordinate frame arrow pointing in Z-axis + mujoco.mjv_initGeom( + viewer.user_scn.geoms[viewer.user_scn.ngeom], + type=mujoco.mjtGeom.mjGEOM_ARROW, + size=[0.02, 0.02, 0.3], # width, thickness, length + pos=[0.0, 0.0, 1.0], + mat=np.eye(3).flatten(), + rgba=[1, 0, 0, 1] # Red arrow + ) + viewer.user_scn.ngeom += 1 + + viewer.sync() + + time_elapsed = time.time() - step_start + if time_elapsed < model.opt.timestep: + time.sleep(model.opt.timestep - time_elapsed) +``` diff --git a/skills/mujoco-engineering/references/tutorial-map.md b/skills/mujoco-engineering/references/tutorial-map.md new file mode 100644 index 0000000..dfd11e9 --- /dev/null +++ b/skills/mujoco-engineering/references/tutorial-map.md @@ -0,0 +1,82 @@ +# MuJoCo Engineering Tutorial Map + +Use repository-relative paths only. Inspect referenced files before coding. + +## Project Setup And Installation + +- Editable Python package and docs site: `README.md`, `pyproject.toml`, `setup.py`, `mujoco_learning_doc/` +- MuJoCo install/source build: `MJCF/Chapter0-install/tutorial.md` +- C++ build patterns: `CPP/Chapter1-make/CMakeLists.txt`, `CPP/Chapter1-make/tutorial.md` + +## MJCF Authoring Targets + +- Base world/options/assets/materials: `MJCF/Chapter2-virtual_world/tutorial.md` +- Body tree, geoms, sites, meshes, coordinate frames: `MJCF/Chapter3-worldbody/tutorial.md` +- Joints: `MJCF/Chapter4-joint/tutorial.md` +- Friction/contact parameters: `MJCF/Chapter5-friction/tutorial.md` +- Actuators: `MJCF/Chapter6-actuator/tutorial.md` +- Light/camera/replicate: `MJCF/Chapter7-light&replicate/tutorial.md` +- Tendons: `MJCF/Chapter8-tendon/tutorial.md` +- Sensors: `MJCF/Chapter9-sensor/tutorial.md` +- CAD import scripts/data: `MJCF/Chapter10-from_CAD_software/` +- Equality constraints: `MJCF/Chapter11-equality/tutorial.md`, `extend/equality/` +- Defaults/classes: `MJCF/Chapter12-default/tutorial.md` +- Composite/flex deformables: `MJCF/Chapter13-composite/tutorial.md`, `MJCF/Chapter14-flex/tutorial.md` +- Keyframes: `MJCF/Chapter15-keyframe/tutorial.md` + +## Python API Examples + +- Viewer/stepping: `Python/Chapter1-view&step/view.py` +- Object IDs and entity lookup: `Python/Chapter2-get_obj/get_obj.py` +- Sensor reads: `Python/Chapter3-sensor_data/sensor_data.py` +- Drawing/debug visualization: `Python/Chapter4-draw/draw.py` +- Force terms: `Python/Chapter5-force/force.py` +- Rendering configuration/segmentation/stereo: `Python/Chapter6-vis_cfg/vis_cfg.py` +- Ray queries: `Python/Chapter7-ray/ray.py` + +## C++ API Examples + +- Viewer/stepping: `CPP/Chapter2-view&step/basic.cc` +- Object IDs and entity lookup: `CPP/Chapter3-get_obj/get_obj.cc` +- Sensor reads: `CPP/Chapter4-sensor_data/sensor_data.cc` +- Drawing/debug visualization: `CPP/Chapter5-draw/draw.cpp` +- Force terms: `CPP/Chapter6-force/force.cpp` +- Rendering configuration: `CPP/Chapter7-vis_cfg/vis_cfg.cpp` +- Ray queries: `CPP/Chapter8-ray/ray.cpp` +- Shared helper: `utils/mujoco_thread/` + +## Extension Examples + +- Touch sensing: `extend/touch/readme.md`, `extend/touch/C++/` +- Soft contact: `extend/soft_contact/tutorial.md`, `extend/soft_contact/soft_contact.py`, `extend/soft_contact/C++/soft_contact.cpp`, related XML files in `extend/soft_contact/` +- Ray caster / depth/range sensing: `extend/deep_camera/readme.md`, `extend/deep_camera/C++/RayCasterCamera.hpp`, external repo `https://github.com/Albusgive/mujoco_ray_caster` +- MJX/JAX experiments: `extend/jax/` +- Entertainment/redstone demo: `fun/mujoco_red_stone/` + +## Validation Commands + +- Python syntax for docs web: `conda run -n mj python -m compileall mujoco_learning_doc` +- Install package in target environment: `python -m pip install -e .` +- Start docs site: `mujoco_learning_doc --port 8000` +- For C++ chapters, follow each chapter's `CMakeLists.txt`; avoid reusing generated `build/`, `b/`, or cache artifacts as source. + +## Minimal Demo And Validation Assets + +Use these repository-relative assets selectively when parameter tuning, reproduction, or validation keeps failing and a smaller isolated demo would clarify the issue: + +- General XML examples: `API-MJCF/`, especially `mecanum.xml`, `force.xml`, `vis_cfg.xml`, `deep_ray.xml` +- MJCF chapter scene files: `MJCF/Chapter*/` +- Python chapter scripts: `Python/Chapter*/` +- C++ chapter examples and `CMakeLists.txt`: `CPP/Chapter*/` +- Soft contact XML/Python/C++ examples: `extend/soft_contact/` +- Ray caster examples: `extend/deep_camera/`, `Python/Chapter7-ray/`, `CPP/Chapter8-ray/` +- Touch/equality/JAX extension examples: `extend/touch/`, `extend/equality/`, `extend/jax/` + +For multimodal agents, a diagnostic demo may include screenshots or video captured from the viewer/render pipeline and compared with the expected visual effect. For text-only agents, produce deterministic textual outputs from simulation state, such as sensor arrays, contact counts, `qpos`, `qvel`, forces, ray hit distances, or rendered image shape/statistics. + +## Official Docs Fallback + +Use official MuJoCo docs when local examples do not answer an API or build detail: + +- Programming/source build: `https://mujoco.readthedocs.io/en/latest/programming/#building-mujoco-from-source` +- Use the XML/API/reference sections on the same site for exact attribute and function semantics. diff --git a/skills/mujoco-engineering/scripts/find_repo.py b/skills/mujoco-engineering/scripts/find_repo.py new file mode 100644 index 0000000..b33f200 --- /dev/null +++ b/skills/mujoco-engineering/scripts/find_repo.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +from pathlib import Path + + +REQUIRED = ("directory.md", "MJCF", "Python", "CPP") +SKILL_ROOT = Path(__file__).resolve().parents[1] +LOCAL_CONFIG = SKILL_ROOT / ".local" / "repo_path.txt" + + +def is_repo_root(path: Path) -> bool: + return all((path / item).exists() for item in REQUIRED) + + +def ancestors(path: Path): + current = path.resolve() + yield current + yield from current.parents + + +def saved_repo_path() -> Path | None: + if not LOCAL_CONFIG.exists(): + return None + value = LOCAL_CONFIG.read_text(encoding="utf-8").strip() + if not value: + return None + return Path(value).expanduser() + + +def candidates() -> list[Path]: + items: list[Path] = [] + saved_root = saved_repo_path() + if saved_root: + items.append(saved_root) + + env_root = os.environ.get("MUJOCO_LEARNING_ROOT") + if env_root: + items.append(Path(env_root).expanduser()) + + for base in ancestors(Path.cwd()): + items.append(base) + + script_path = Path(__file__).resolve() + items.extend(script_path.parents) + + home = Path.home() + items.extend( + [ + home / "mujoco_learning", + home / "mujoco-learning", + home / "code" / "mujoco_learning", + home / "projects" / "mujoco_learning", + home / "workspace" / "mujoco_learning", + ] + ) + return items + + +def save_repo_path(path: Path) -> int: + root = path.expanduser().resolve() + if not is_repo_root(root): + print(f"Invalid MuJoCo tutorial repository path: {root}") + print("Expected directory.md plus MJCF/, Python/, and CPP/ under that path.") + return 1 + LOCAL_CONFIG.parent.mkdir(parents=True, exist_ok=True) + LOCAL_CONFIG.write_text(str(root), encoding="utf-8") + print(root) + return 0 + + +def clear_repo_path() -> int: + if LOCAL_CONFIG.exists(): + LOCAL_CONFIG.unlink() + print("Cleared saved MuJoCo tutorial repository path.") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Find or configure the local MuJoCo tutorial repository.") + parser.add_argument("--set", dest="set_path", help="Save the local MuJoCo tutorial repository path.") + parser.add_argument("--clear", action="store_true", help="Clear the saved repository path.") + args = parser.parse_args() + + if args.clear: + return clear_repo_path() + if args.set_path: + return save_repo_path(Path(args.set_path)) + + for item in candidates(): + if is_repo_root(item): + print(item.resolve()) + return 0 + + if LOCAL_CONFIG.exists(): + print("Saved MuJoCo tutorial repository path is invalid or unavailable. Ask the user for the new clone path and run this script with --set PATH.") + return 1 + + print( + "MuJoCo tutorial repository not found. Ask the user for the local clone path and run this script with --set PATH.", + end="", + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/mujoco-teaching/SKILL.md b/skills/mujoco-teaching/SKILL.md new file mode 100644 index 0000000..8b05950 --- /dev/null +++ b/skills/mujoco-teaching/SKILL.md @@ -0,0 +1,59 @@ +--- +name: mujoco-teaching +description: Use when a user wants to learn MuJoCo concepts, MJCF modeling, Python/C++ MuJoCo APIs, sensors, rendering, contact, soft contact, ray casting, or asks conceptual/tutorial questions about this repository's MuJoCo lessons. Prefer the repository tutorials first, then consult official MuJoCo documentation if the tutorial is incomplete. +--- + +# MuJoCo Teaching + +Use this skill for human learning: explain MuJoCo concepts, answer tutorial questions, compare API choices, or guide a learner through MJCF/Python/C++ examples. + +## Source Priority + +1. Use this repository's tutorial material first. +2. If the repository does not fully answer the question, consult official MuJoCo docs, especially: + `https://mujoco.readthedocs.io/en/latest/programming/#building-mujoco-from-source` +3. Clearly say when an explanation comes from the local tutorial, the official docs, or your own inference. + +## Locate The Tutorial Repository + +Do not assume an absolute path. Find the repository root dynamically: + +- If the current workspace contains `directory.md` and folders `MJCF/`, `Python/`, `CPP/`, use the current workspace root. +- If this skill is installed inside the repository under `skills/mujoco-teaching`, the repository root is two directories above the skill folder. +- If installed globally, run `scripts/find_repo.py` from this skill. It checks the skill-local saved path, current workspace, `MUJOCO_LEARNING_ROOT`, and common clone directory names. +- On first use, if `scripts/find_repo.py` cannot find the repository, ask the user for the local clone path and save it with `scripts/find_repo.py --set PATH`. +- If the saved path becomes invalid or unavailable, ask again and rerun `scripts/find_repo.py --set PATH`. +- If no local clone is available, use the public repository URL the user provides. + +The saved path lives in `.local/repo_path.txt` under the installed skill directory. This is a cross-platform local config file; do not edit `SKILL.md` to store user paths. + +All paths in this skill are repository-relative. + +## Workflow + +1. Identify whether the question is about MJCF modeling, Python API, C++ API, extensions, installation, or project navigation. +2. Check `references/quick-reference.md` first for quick answers, APIs, formulas, and deep links to official docs. +3. Read `references/tutorial-map.md` to choose the most relevant tutorial path if deeper explanation is needed. +4. Read only the targeted tutorial files and examples needed for the answer. +5. Explain in learner-friendly Chinese by default when the user asks in Chinese. +6. Include small code/XML snippets only when they clarify the concept. +7. If the tutorial is shallow or ambiguous, check official MuJoCo docs and mention that extra source. + +## Teaching Style + +- Keep answers concise and compact. If the active agent is framed as a chatbot, avoid excessive headings, blank lines, and long segmented explanations; prefer dense short paragraphs or a small bullet list. +- When mentioning a dependency, official API, external project, or useful reference, include a clickable link when one is known. +- When referencing this tutorial repository in a chat-only context, prefer GitHub links to project files instead of local absolute paths. Use repository-relative paths only when working inside a local clone. +- For ray caster / depth camera / ray-based ranging questions, mention the maintained project when relevant: [Albusgive/mujoco_ray_caster](https://github.com/Albusgive/mujoco_ray_caster). +- Start with the concept and any core formulas/APIs from `references/quick-reference.md`, then map it to the repository's example path. +- Prefer "why this works" over just listing API calls. +- **Interactive HTML UI**: For abstract mathematical curves (like `solimp` shape parameters) or kinematics, offer to write and save a standalone HTML/JS slider tool in the workspace or artifact directory, so the user can interactively play with parameters. +- **Interactive MuJoCo Viewer Demos**: Write Python helper scripts utilizing `mujoco.viewer.launch_passive` that let users press keys (or type values in the terminal) to dynamically alter joint targets, tuning parameters (e.g. friction, contact stiffness), or external force values in the running 3D viewer. +- **Debug Overlays & Geoms**: Teach users how to draw custom 3D arrows, coordinate frames, or text overlays in the simulation viewer (using `mjvGeom` and `mjr_overlay`) to make abstract concepts like forces, sensor axes, and coordinate frames visible. +- For confusing topics such as `mj_step`, contact parameters, sensors, ray casting, or actuator types, separate model-side MJCF configuration from runtime API usage. +- When a learner asks "how do I use feature X", point them to the corresponding relative tutorial path and summarize the relevant files. + +## Reference Map + +- Quick cheatsheet, APIs, formulas, and deep links: `references/quick-reference.md` +- Detailed file mapping: `references/tutorial-map.md` diff --git a/skills/mujoco-teaching/agents/openai.yaml b/skills/mujoco-teaching/agents/openai.yaml new file mode 100644 index 0000000..f7ecbfb --- /dev/null +++ b/skills/mujoco-teaching/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "MuJoCo Teaching" + short_description: "Explain MuJoCo using this tutorial repo" + brand_color: "#0F766E" + default_prompt: "Use $mujoco-teaching to explain MuJoCo sensors from the local tutorial and official docs." +policy: + allow_implicit_invocation: true diff --git a/skills/mujoco-teaching/references/quick-reference.md b/skills/mujoco-teaching/references/quick-reference.md new file mode 100644 index 0000000..e526d77 --- /dev/null +++ b/skills/mujoco-teaching/references/quick-reference.md @@ -0,0 +1,374 @@ +# MuJoCo Quick Reference & Essence (速查手册与精华提炼) + +This document contains key formulas, API usage patterns, configuration references, and official documentation deep links. Use this for quick lookup during coding and explanation. + +--- + +## 1. MJCF Modeling Quick Reference (建模速查) + +### 1.1 Soft Contact (软接触与求解器) +MuJoCo simulates soft contacts using a dynamic "spring-damper" model. +* **Equation**: $a_{ref} = -b v - k r$ (where $r$ is penetration depth, $v$ is velocity, $b$ is damping, $k$ is stiffness). +* **solimp**: Calculates the constraint impedance $d(r) \in (0, 1)$. + * Attributes: `(d0, dwidth, width, midpoint, power)` + * Default: `(0.9, 0.95, 0.001, 0.5, 2)` + * XML Syntax: `` + * Official Reference: [MuJoCo option-solimp](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-solimp) | [Computation Contacts](https://mujoco.readthedocs.io/en/latest/computation/index.html#contacts) +* **solref**: Calculates stiffness $k$ and damping $b$. + * Positive values: `(timeconst, dampratio)`. $b = \frac{2}{d_{width} \cdot timeconst}$, $k = \frac{d(r)}{d_{width} \cdot timeconst^2 \cdot dampratio^2}$. + * Negative values: `(-stiffness, -damping)`. $b = \frac{damping}{d_{width}}$, $k = \frac{stiffness \cdot d(r)}{d_{width}^2}$. + * XML Syntax: `` or `` + * Official Reference: [MuJoCo option-solref](https://mujoco.readthedocs.io/en/latest/XMLreference.html#option-solref) +* **Tuning Rules**: + * For rubber/bouncy materials: set a larger `width` (e.g., 0.05) and adjust `solref` to control elasticity. + * To prevent interpenetration (穿模): increase stiffness $k$ (by decreasing `timeconst` or increasing `-stiffness`). If it jitters, increase damping $b$. + +### 1.2 Joints, Actuators & Sensors +* **Joints**: `` + * Types: `hinge` (旋转), `slide` (滑动), `ball` (球形/三自由度), `free` (自由度/六自由度). + * Official Reference: [MuJoCo joint](https://mujoco.readthedocs.io/en/latest/XMLreference.html#joint) +* **Actuators**: + * Torque control: `` + * Position control: `` + * Velocity control: `` + * Official Reference: [MuJoCo actuator](https://mujoco.readthedocs.io/en/latest/XMLreference.html#actuator) +* **Sensors**: + * IMU / Gyro / Accel: ``, `` + * Touch sensor: `` + * Force / Torque: ``, `` + * Official Reference: [MuJoCo sensor](https://mujoco.readthedocs.io/en/latest/XMLreference.html#sensor) + +--- + +## 2. Python API Quick Reference (Python API) + +### 2.1 Model Loading & Stepping (加载与步进) +```python +import mujoco + +# Load model and create runtime data +model = mujoco.MjModel.from_xml_path("scene.xml") +data = mujoco.MjData(model) + +# Simulation step +mujoco.mj_step(model, data) +``` +* Official Reference: [MuJoCo Python Programming](https://mujoco.readthedocs.io/en/latest/programming.html#initialization) + +### 2.2 Object Lookup (对象查名/ID) +```python +# Convert name to ID +geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "geom_name") +if geom_id == -1: + raise ValueError("Geom not found") + +# Get name from ID +geom_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_GEOM, geom_id) +``` +* Official Reference: [Indices and Names](https://mujoco.readthedocs.io/en/latest/programming.html#indices-and-names) + +### 2.3 Sensor Reading (读取传感器) +* **New and Preferred Way (Attribute Access)**: + ```python + sensor_val = data.sensor("sensor_name").data + ``` +* **Classic Way (Index Access)**: + ```python + sensor_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, "sensor_name") + start_idx = model.sensor_adr[sensor_id] + dim = model.sensor_dim[sensor_id] + sensor_val = data.sensordata[start_idx : start_idx + dim] + ``` +* Official Reference: [MuJoCo Sensor Programming](https://mujoco.readthedocs.io/en/latest/programming.html#sensors) + +### 2.4 Offscreen Rendering & Camera Pixels (相机画面读取) +```python +import glfw +import numpy as np + +# Initialize GLFW and hidden window +glfw.init() +glfw.window_hint(glfw.VISIBLE, glfw.FALSE) +window = glfw.create_window(640, 480, "offscreen", None, None) +glfw.make_context_current(window) + +# Setup renderer and camera +camera = mujoco.MjvCamera() +camera.type = mujoco.mjtCamera.mjCAMERA_FIXED +camera.fixedcamid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "my_camera") + +scene = mujoco.MjvScene(model, maxgeom=1000) +context = mujoco.MjrContext(model, mujoco.mjtFontScale.mjFONTSCALE_150) +mujoco.mjr_setBuffer(mujoco.mjtFramebuffer.mjFB_OFFSCREEN, context) + +# Render & read pixels +viewport = mujoco.MjrRect(0, 0, 640, 480) +mujoco.mjv_updateScene(model, data, mujoco.MjvOption(), None, camera, mujoco.mjtCatBit.mjCAT_ALL, scene) +mujoco.mjr_render(viewport, scene, context) + +rgb = np.zeros((480, 640, 3), dtype=np.uint8) +depth = np.zeros((480, 640), dtype=np.float32) +mujoco.mjr_readPixels(rgb, depth, viewport, context) +# Flip vertically because OpenGL coordinates start from bottom-left +rgb = np.flipud(rgb) +depth = np.flipud(depth) +``` +* Official Reference: [MuJoCo Visualization Programming](https://mujoco.readthedocs.io/en/latest/programming.html#visualization) + +--- + +## 3. C++ API & Compilation Quick Reference (C++ 编译与开发) + +### 3.1 C++ Simulation Loop (基础C++仿真循环) +```cpp +#include +#include + +mjModel* m = nullptr; +mjData* d = nullptr; + +int main() { + char error[1000]; + m = mj_loadXML("scene.xml", nullptr, error, 1000); + if (!m) { + mju_error("Could not load model: %s", error); + } + d = mj_makeData(m); + + // Main step loop + while (!glfwWindowShouldClose(window)) { + mj_step(m, d); + } + + mj_deleteData(d); + mj_deleteModel(m); + return 0; +} +``` + +### 3.2 Minimum CMakeLists.txt (C++ 最小编译模板) +```cmake +cmake_minimum_required(VERSION 3.20) +project(mujoco_cpp_example) + +# Option A: From CMake install directory (e.g., /opt/mujoco) +set(MUJOCO_FOLDER /opt/mujoco/lib/cmake) +find_package(mujoco REQUIRED PATHS ${MUJOCO_FOLDER} NO_DEFAULT_PATH) + +add_executable(basic basic.cc) +target_link_libraries(basic mujoco::mujoco glut GL GLU glfw) +``` +* Official Reference: [Building MuJoCo from source](https://mujoco.readthedocs.io/en/latest/programming.html#building-mujoco-from-source) + +--- + +## 4. Common Pitfalls & Solutions (避坑指南) + +* **GLIBCXX not found in Conda**: + * *Error*: `ImportError: /lib/x86_64-linux-gnu/libstdc++.so.6: version 'GLIBCXX_3.4.30' not found` + * *Fix*: Run `conda install -c conda-forge libstdcxx-ng` inside the activated conda environment. +* **Header Include Order in C++**: + * *Pitfall*: Including OpenGL or GLFW headers *before* MuJoCo headers can sometimes cause macro redefinition errors. + * *Fix*: Always include `` first. +* **GLFW Context in Headless Servers**: + * *Pitfall*: Calling `glfw.create_window` on Linux headless servers without setting `glfw.window_hint(glfw.VISIBLE, glfw.FALSE)` or without X11 server will crash. + * *Fix*: Use `os.environ["DISPLAY"]` checks or use EGL/OSMesa instead of GLFW if graphics driver is absent. + +--- + +## 5. Interactive & Visual Teaching Templates (交互式与仿真式教学模板) + +### 5.1 Standalone HTML Slider Visualizer (HTML 数学曲线交互分析模板) +Generate and save this template as an HTML file in the workspace or artifact directory when teaching curves like `solimp` or spring-damper equations: +```html + + + + + MuJoCo solimp 曲线交互可视化 + + + + + + MuJoCo solimp (阻抗曲线) 交互分析器 + 公式: $d(r) = d_0 + Y(r/width) \cdot (d_{width} - d_0)$,调节下方参数滑块观察阻抗如何随穿模深度 $r$ 动态变化: + + + d0 (起始阻抗) + + 0.9 + + + dwidth (最大阻抗) + + 0.95 + + + width (归一化宽度) + + 0.001 + + + midpoint (分界点) + + 0.5 + + + power (曲线幂指数) + + 2 + + + + + + + + +``` + +### 5.2 Python Passive Viewer Keyboard Interaction (`launch_passive` 键盘交互仿真) +Write this script when teaching users how to interactively test control loops, apply forces, or tune friction dynamically inside MuJoCo's 3D viewer: +```python +import mujoco +import mujoco.viewer +import time +import sys + +# Load model (make sure you have an actuator or control joint) +model = mujoco.MjModel.from_xml_path("API-MJCF/mecanum.xml") +data = mujoco.MjData(model) + +# Active keyboard control loop +with mujoco.viewer.launch_passive(model, data) as viewer: + print("\n=== 交互控制台 ===") + print("按下 [W] 增加控制目标,按下 [S] 减小控制目标") + print("按下 [Q] 退出仿真") + + target_ctrl = 0.0 + + while viewer.is_running(): + step_start = time.time() + + # Check key states (viewer.user_btn or custom key handlers if running window) + # Note: In launch_passive, you can listen to terminal or use window events. + # Alternatively, modify data.ctrl directly based on terminal triggers: + # data.ctrl[0] = target_ctrl + + mujoco.mj_step(model, data) + viewer.sync() + + # Step rate limiter + time_elapsed = time.time() - step_start + if time_elapsed < model.opt.timestep: + time.sleep(model.opt.timestep - time_elapsed) +``` + +### 5.3 Custom 3D Debug Overlays (绘制自定义调试几何形状) +Use this boilerplate to teach users how to draw custom geometric objects (arrows, spheres, coordinate axes) dynamically in the 3D viewer without modifying the XML file: +```python +import mujoco +import mujoco.viewer +import numpy as np +import time + +model = mujoco.MjModel.from_xml_path("API-MJCF/force.xml") +data = mujoco.MjData(model) + +with mujoco.viewer.launch_passive(model, data) as viewer: + while viewer.is_running(): + step_start = time.time() + mujoco.mj_step(model, data) + + # Draw a custom 3D arrow to visualize contact force or direction + # ngeoms should be reset or incremented on user_scn + viewer.user_scn.ngeom = 0 # Clear previous custom shapes + + # Add a custom sphere at coordinates (0, 0, 1) + mujoco.mjv_initGeom( + viewer.user_scn.geoms[viewer.user_scn.ngeom], + type=mujoco.mjtGeom.mjGEOM_SPHERE, + size=[0.05, 0, 0], + pos=[0.0, 0.0, 1.0], + mat=np.eye(3).flatten(), + rgba=[0, 1, 0, 0.8] # Semi-transparent green + ) + viewer.user_scn.ngeom += 1 + + # Add a custom coordinate frame arrow pointing in Z-axis + mujoco.mjv_initGeom( + viewer.user_scn.geoms[viewer.user_scn.ngeom], + type=mujoco.mjtGeom.mjGEOM_ARROW, + size=[0.02, 0.02, 0.3], # width, thickness, length + pos=[0.0, 0.0, 1.0], + mat=np.eye(3).flatten(), + rgba=[1, 0, 0, 1] # Red arrow + ) + viewer.user_scn.ngeom += 1 + + viewer.sync() + + time_elapsed = time.time() - step_start + if time_elapsed < model.opt.timestep: + time.sleep(model.opt.timestep - time_elapsed) +``` diff --git a/skills/mujoco-teaching/references/tutorial-map.md b/skills/mujoco-teaching/references/tutorial-map.md new file mode 100644 index 0000000..8223ea1 --- /dev/null +++ b/skills/mujoco-teaching/references/tutorial-map.md @@ -0,0 +1,63 @@ +# MuJoCo Tutorial Map + +Use repository-relative paths only. Do not invent absolute paths. + +## Core Entry Points + +- `README.md`: project overview, local docs site, installation commands. +- `directory.md`: human-readable tutorial table of contents. +- `MJCF/Chapter0-install/tutorial.md`: installing MuJoCo from source/release/Python. + +## MJCF Modeling Lessons + +- World and visualization setup: `MJCF/Chapter2-virtual_world/tutorial.md` +- Bodies, geoms, sites, coordinate tree: `MJCF/Chapter3-worldbody/tutorial.md` +- Joints and joint parameters: `MJCF/Chapter4-joint/tutorial.md` +- Friction and contact dimensions: `MJCF/Chapter5-friction/tutorial.md` +- Actuators and control types: `MJCF/Chapter6-actuator/tutorial.md` +- Lights, cameras, replicate, ray demos: `MJCF/Chapter7-light&replicate/tutorial.md` +- Tendons and muscle-like actuation: `MJCF/Chapter8-tendon/tutorial.md` +- Sensors, camera, IMU, velocity, angles: `MJCF/Chapter9-sensor/tutorial.md` +- CAD model import workflow: `MJCF/Chapter10-from_CAD_software/tutorial.md` +- Equality constraints and parallel mechanisms: `MJCF/Chapter11-equality/tutorial.md` +- Defaults/classes/inheritance: `MJCF/Chapter12-default/tutorial.md` +- Old composite deformables: `MJCF/Chapter13-composite/tutorial.md` +- New flex deformables: `MJCF/Chapter14-flex/tutorial.md` +- Keyframes: `MJCF/Chapter15-keyframe/tutorial.md` + +## Python API Lessons + +- Viewer, model/data, stepping: `Python/Chapter1-view&step/tutorial.md`; code: `Python/Chapter1-view&step/view.py` +- Object/entity lookup: `Python/Chapter2-get_obj/tutorial.md`; code: `Python/Chapter2-get_obj/get_obj.py` +- Sensor data access: `Python/Chapter3-sensor_data/tutorial.md`; code: `Python/Chapter3-sensor_data/sensor_data.py` +- 2D/3D drawing: `Python/Chapter4-draw/tutorial.md`; code: `Python/Chapter4-draw/draw.py` +- Force terms and validation: `Python/Chapter5-force/tutorial.md`; code: `Python/Chapter5-force/force.py` +- Rendering configuration, segmentation, stereo: `Python/Chapter6-vis_cfg/tutorial.md`; code: `Python/Chapter6-vis_cfg/vis_cfg.py` +- Ray distance queries: `Python/Chapter7-ray/tutorial.md`; code: `Python/Chapter7-ray/ray.py` + +## C++ API Lessons + +- Build and CMake: `CPP/Chapter1-make/tutorial.md`; code: `CPP/Chapter1-make/` +- Viewer and stepping: `CPP/Chapter2-view&step/tutorial.md`; code: `CPP/Chapter2-view&step/basic.cc` +- Object/entity lookup: `CPP/Chapter3-get_obj/tutorial.md`; code: `CPP/Chapter3-get_obj/get_obj.cc` +- Sensor data access: `CPP/Chapter4-sensor_data/tutorial.md`; code: `CPP/Chapter4-sensor_data/sensor_data.cc` +- 2D/3D drawing: `CPP/Chapter5-draw/tutorial.md`; code: `CPP/Chapter5-draw/draw.cpp` +- Force terms and validation: `CPP/Chapter6-force/tutorial.md`; code: `CPP/Chapter6-force/force.cpp` +- Rendering configuration: `CPP/Chapter7-vis_cfg/tutorial.md`; code: `CPP/Chapter7-vis_cfg/vis_cfg.cpp` +- Ray distance queries: `CPP/Chapter8-ray/tutorial.md`; code: `CPP/Chapter8-ray/ray.cpp` + +## Extensions And Special Topics + +- Touch sensing: `extend/touch/readme.md` +- Soft contact and solver parameter intuition: `extend/soft_contact/tutorial.md` +- Ray caster / depth/range sensing: `extend/deep_camera/readme.md`; external repo: `https://github.com/Albusgive/mujoco_ray_caster` +- Equality experiments: `extend/equality/` +- JAX/MJX examples: `extend/jax/` +- Entertainment/redstone demo: `fun/mujoco_red_stone/README.md` + +## Official Docs Fallback + +Use official MuJoCo docs when local material is incomplete: + +- Programming guide and source build: `https://mujoco.readthedocs.io/en/latest/programming/#building-mujoco-from-source` +- XML reference, API reference, and computation chapters from the same documentation site as needed. diff --git a/skills/mujoco-teaching/scripts/find_repo.py b/skills/mujoco-teaching/scripts/find_repo.py new file mode 100644 index 0000000..b33f200 --- /dev/null +++ b/skills/mujoco-teaching/scripts/find_repo.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +from pathlib import Path + + +REQUIRED = ("directory.md", "MJCF", "Python", "CPP") +SKILL_ROOT = Path(__file__).resolve().parents[1] +LOCAL_CONFIG = SKILL_ROOT / ".local" / "repo_path.txt" + + +def is_repo_root(path: Path) -> bool: + return all((path / item).exists() for item in REQUIRED) + + +def ancestors(path: Path): + current = path.resolve() + yield current + yield from current.parents + + +def saved_repo_path() -> Path | None: + if not LOCAL_CONFIG.exists(): + return None + value = LOCAL_CONFIG.read_text(encoding="utf-8").strip() + if not value: + return None + return Path(value).expanduser() + + +def candidates() -> list[Path]: + items: list[Path] = [] + saved_root = saved_repo_path() + if saved_root: + items.append(saved_root) + + env_root = os.environ.get("MUJOCO_LEARNING_ROOT") + if env_root: + items.append(Path(env_root).expanduser()) + + for base in ancestors(Path.cwd()): + items.append(base) + + script_path = Path(__file__).resolve() + items.extend(script_path.parents) + + home = Path.home() + items.extend( + [ + home / "mujoco_learning", + home / "mujoco-learning", + home / "code" / "mujoco_learning", + home / "projects" / "mujoco_learning", + home / "workspace" / "mujoco_learning", + ] + ) + return items + + +def save_repo_path(path: Path) -> int: + root = path.expanduser().resolve() + if not is_repo_root(root): + print(f"Invalid MuJoCo tutorial repository path: {root}") + print("Expected directory.md plus MJCF/, Python/, and CPP/ under that path.") + return 1 + LOCAL_CONFIG.parent.mkdir(parents=True, exist_ok=True) + LOCAL_CONFIG.write_text(str(root), encoding="utf-8") + print(root) + return 0 + + +def clear_repo_path() -> int: + if LOCAL_CONFIG.exists(): + LOCAL_CONFIG.unlink() + print("Cleared saved MuJoCo tutorial repository path.") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Find or configure the local MuJoCo tutorial repository.") + parser.add_argument("--set", dest="set_path", help="Save the local MuJoCo tutorial repository path.") + parser.add_argument("--clear", action="store_true", help="Clear the saved repository path.") + args = parser.parse_args() + + if args.clear: + return clear_repo_path() + if args.set_path: + return save_repo_path(Path(args.set_path)) + + for item in candidates(): + if is_repo_root(item): + print(item.resolve()) + return 0 + + if LOCAL_CONFIG.exists(): + print("Saved MuJoCo tutorial repository path is invalid or unavailable. Ask the user for the new clone path and run this script with --set PATH.") + return 1 + + print( + "MuJoCo tutorial repository not found. Ask the user for the local clone path and run this script with --set PATH.", + end="", + ) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())
公式: $d(r) = d_0 + Y(r/width) \cdot (d_{width} - d_0)$,调节下方参数滑块观察阻抗如何随穿模深度 $r$ 动态变化: