這篇文章是 Mujoco 學習系列第二篇,主要介紹一些基礎功能與 xmI 使用,重點在于如何編寫與讀懂 xml 文件。
運行這篇博客前請先確保正確安裝 Mujoco 并通過了基本功能與GUI的驗證,即至少完整下面這個博客的 第二章節 內容:
- Mujoco 學習系列(一)安裝與部署
1. 啟動仿真器
在第一篇博客中已經介紹了如何通過命令啟動仿真器,但實際上mujoco提供了很多種方式啟動,不同啟動方式在后面的工作中會有不同的作用。
1.1 python 命令行啟動
- 只啟動仿真器,不加載模型
(mojoco) $ python -m mujoco.viewer
- 啟動仿真器,同時加載模型(這里加載自帶的小車模型)
(mojoco) $ python -m mujoco.viewer --mjcf=./model/car/car.xml
1.2 python 腳本啟動
- 啟動仿真器,不加載模型(阻塞)
import mujocomojoco.viewer.launch()
- 啟動仿真器,同時加載模型(阻塞)
import mujocomodel_xml_path = "./model/car/car.xml"
mujoco.viewer.launch_from_path(model_xml_path)
上面兩種阻塞方式啟動后 terminal 會一直等待你在仿真器中操作完并關閉。mujoco 也提供了非阻塞方式啟動仿真器 launch_passive(model, data)
,但在啟動時必須將模型加載進來,同時需要手動管理 mj_step()
函數,而以阻塞方式啟動的仿真器不需要顯示調用該函數。因為是非阻塞方式啟動,需要將仿真器放在一個循環中,否則一啟動就會立刻關閉。
- 啟動仿真器,同時加載模型(非阻塞)
import timeimport mujoco
import mujoco.viewermodel_xml_path = "./model/car/car.xml"
model = mujoco.MjModel.from_xml_path(model_xml_path)
data = mujoco.MjData(model)with mujoco.viewer.launch_passive(model=model, data=data) as viewer:while viewer.is_running():step_start = time.time() # 每一幀仿真的開始時間,用于控制仿真的時間步長mujoco.mj_step(model, data) # [核心] 手動推進一次仿真# 給 simulate GUI 加鎖,防止數據修改線程與渲染線程出現沖突with viewer.lock():# 這行主要是提升交互體驗,你在運行后可以發現環境中每個接觸點都會有黃色的圓柱在閃爍viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(data.time % 2)viewer.sync() # 將最新的數據同步給GUI中并顯示# 到達此處說明當前幀的運算和渲染已經結束了,計算一下到下一幀的時間間隔,用于控制仿真節奏# 如果仿真在這一步消耗了很長時間,那么該值是有可能為負time_until_next_step = model.opt.timestep - (time.time() - step_start)if time_until_next_step > 0:time.sleep(time_until_next_step) # 休眠一下后準備計算下一幀
運行之后就可以看到上圖中的效果,可以發現在小車三個輪子的位置處有黃色的矮圓柱體在閃爍,這就是代碼中 viewer.opt.flags
部分起的作用。
2. 編寫 xml 文件
在上一章節中其實遺留了一個問題:多個模型的加載在機器人仿真中是必要的,但 mujoco 本身是不支持同時加載多個 xml
文件的,因為 mujoco 是 面向單物理場景 設計的,只不過有方法來實現這點,上面例子中的小車本質上就是在一個 xml 文件中創建了不同元素并將其組合,多個模型加載問題可以被轉化成不同元素但不進行組合。
在編寫之前需要先理解 mujoco 如何解析 xml 文件的,特別是哪些標簽是核心的、哪些是可以以類形式定義等。
- 官方解釋鏈接:XMLreference.html
我將 xml 的標簽分為兩類:
- 環境標簽:這類標簽定義了全局仿真配置,包括重力、第三人稱相機視角、密度、時間步長等;
- 對象標簽:這類標簽是可以繼承、包含、相互作用,有點類似代碼中的 class;
這種分類方式實際上不嚴謹,但對于初學者而言可以先這樣簡化地去理解。
2.1 立方體與平面
首先是最簡單的一個例子,在一個空間中有一個立方體和一個平面,期望立方體自由落體后掉在平面上:
<mujoco><!-- 場景 --><worldbody><!-- 光源 --><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <!-- 平面 --><geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><!-- 對象 --><body name="cube" pos="0 0 1"><joint type="free"/> <!-- 該對象與外界的鏈接方式 --><geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/></body></worldbody>
</mujoco>
上面的例子中頂級標簽為 <mujoco>
,其他所有對象都在 <worldbody>
標簽下,在這個標簽中有兩個對象 平面 和 立方體。
【Note】:雖然mujoco官方文檔和很多教程都告訴你可以省略 name
字段,沒有顯示聲明的情況下會自動分配一個匿名值,但我會將 name
字段一直寫上去,因為 mujoco 不允許出現同名對象,同時有 name
字段可以幫你更快定位到問題。
直接運行就可以看到一個立方體自由落體到平面上:
(mujoco) $ python -m mujoco.viewer --mjcf=./merge.xml
實際上在xml中的 平面 也是一個對象,但我個人習慣地面對象不用 body
標簽包裹以區分運動對象和地面,所以下面的寫法也是正確的:
<mujoco><!-- 場景 --><worldbody><!-- 光源 --><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <!-- 平面 --><body name="ground" pos="0 0 0" ><geom type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/></body><!-- 對象 --><body name="cube" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/> <!-- 對象的幾何形狀 --></body></worldbody>
</mujoco>
同理,如果想要在不同位置添加一個新的立方體則如下所示,新的立方體中多了一個 euler
屬性表示其初始角度信息,更多屬性以及其默認值可以在官網文檔中找到:
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><!-- 立方體1 --><body name="cube1" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/> </body><!-- 立方體2 --><body name="cube2" pos="0.5 0.5 0.5" euler="0 20 30"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" rgba="0.5 0.0 0.0 1"/></body></worldbody>
</mujoco>
運行后也是兩個立方體自由落體,只不過初試高度和角度不同,因此最終落地姿勢也不同。
(mujoco) $ python -m mujoco.viewer --mjcf=./merge.xml
2.2 環境標簽
通常情況下環境標簽是在第一步就需要做的,為了避免不同平臺中存在差異,雖然 mujoco 允許在運行過程中修改環境標簽的值,例如更改重力方向,但在沒有特殊需求的情況下這些值應該被定義為一個靜態值。
在 mujoco 中通過 xml 里的 <option>
標簽定義環境,可以修改的屬性值有以下幾個:
我最常用的是下面幾個:
- timestep:仿真時間步長,默認 0.002 0.002 0.002s,影響計算速度與精度的最重要參數;
- gravity:重力方向,默認 ( 0 , 0 , ? 9.81 ) (0,0,-9.81) (0,0,?9.81);
- density:環境介質密度,默認 0 0 0,可以修改你的仿真環境是在水下還是空氣中,默認在空氣中;
xml 文件示例如下:
<mujoco><!-- 環境標簽 --><option gravity="0 0 -1" /><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><body name="cube" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/> </body></worldbody>
</mujoco>
【Note】:注意環境標簽 <option>
的位置,由于環境標簽是全局作用的,因此需要將其放在頂級域名之下。
2.3 單位與軸
因為存在 萬向節死鎖 的問題,有些算法會調整 rpy
的旋轉順序,mujoco 提供了標簽 <compile>
來定義單位與軸旋轉順序;在沒有明確定義的情況下度數單位為 弧度,但也可以通過修改來確定度的單位為 角度;
示例如下:
<mujoco><compiler angle="degree" eulerseq="yzx"/><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><body name="cube" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" rgba="0.0 0.0 0.5 1"/> </body></worldbody></mujoco>
這些本質上和環境標簽是同一類型,都是確定好后不會頻繁變化的,因此在曾經結構上也是頂級位置。
2.4 通用資產定義
有些屬性或變量可能會被多個對象使用,如果每個對象都重新寫一遍會非常冗余,mujoco 提供了 <asset>
標簽用來定義通用資產,并且允許對象直接使用。
<mujoco><asset><!-- 定義材質 --><material name="blue" rgba="0 0 0.5 1"/><!-- 定義凸包 --> <mesh name="tetrahedron" vertex="0 0 0 1 0 0 0 1 0 0 0 1"/></asset><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><body name="cube" pos="0 0 1"><joint type="free"/><!-- 使用材質和凸包 --><geom type="box" size="0.1 0.1 0.1" material="blue" mesh="tetrahedron"/> </body></worldbody></mujoco>
上面的示例中使用了 mesh
這個標簽,本質是表面網格,但 網格也可以定義成不帶面的網格(本質上是點云)。在這種情況下,即使編譯器屬性 convexhull
為 false,凸包也會自動構建。這使得直接在 XML 中構建簡單形狀變得非常簡單。例如,可以如下創建金字塔:
如果你想要使用 mesh
原本的含義,即物體表面渲染方式,那么這樣寫即可:
<mujoco><asset><material name="blue" rgba="0 0 0.5 1"/><!-- 前提是同級目錄下有這個文件 --><mesh name="forearm" file="forearm.stl"/></asset><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><body name="cube" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" material="blue" mesh="forearm"/> </body></worldbody></mujoco>
【Note】:由于 mujoco 不允許相同的 name
屬性,因此定義 <asset>
標簽時可以用一些帶有前綴的變量,如 name="asset_blue"
,這樣可以避免在復雜工程中存在名稱沖突的情況,特別是有些廠商提供的模型文件中也定義了顏色和材質等對象。
2.5 文件包含
如果把所有的配置都寫在一個文件中會非常難以梳理,mujoco 提供了 <include>
標簽實現文件包含,我通常會將環境、單位、通用資產的定義寫在一個 common.xml
文件中,在主文件中只關注對象的運動關系:
【Note】:所有包含與被包含文件中的元素都必須在 <mujoco>
這個根標簽下,這是mujoco識別的依據。
- common.xml 文件
<mujoco><asset><material name="blue" rgba="0 0 0.5 1"/></asset>
</mujoco>
- merge.xml 文件
<mujoco><!-- 包含公共變量與資產 --><include file="./common.xml"/> <worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/> <geom name="ground" type="plane" size="1 1 0.1" rgba="0.5 0.5 0.5 1"/><body name="cube" pos="0 0 1"><joint type="free"/><geom type="box" size="0.1 0.1 0.1" material="blue"/> </body></worldbody></mujoco>
2.6 關節約束
重頭戲來了 <joint>
關節約束。mujoco 對關節約束的定義和 urdf 文件基本一致,允許一下幾種形式的約束:
- free:三個平移自由度 + 三個旋轉自由度;
- ball:三個旋轉自由度的球形關節,四元數 (1,0,0,0) 對應于定義初始狀態;
- slide:一個平移自由度的滑動或平移關節,需要明確平移方向;
- hinge:鉸鏈類型創建具有一個旋轉自由度的鉸鏈關節,需要明確旋轉軸;
【Note】:為了更好的交互效果,這里提前引入了<actuator>
標簽,否則無法在仿真器中拖拽。
示例如下:
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 3" dir="0 0 -1"/><geom name="ground" type="plane" size="5 5 0.1" rgba="0.5 0.5 0.5 1" friction="0.1 0.05 0.05"/><!-- 可拖拽立方體 --><body name="draggable_cube" pos="0 0 1"><joint name="x_slide" type="slide" axis="1 0 0" damping="5" stiffness="50" range="-3 3"/><joint name="y_slide" type="slide" axis="0 1 0" damping="5" stiffness="50" range="-3 3"/><geom name="cube" type="box" size="0.1 0.1 0.1" rgba="0 0.5 0.8 1" mass="5"/></body></worldbody><actuator><!-- 位置伺服控制器 --><position name="x_pos" joint="x_slide" kp="500" kv="20"/><position name="y_pos" joint="y_slide" kp="500" kv="20"/></actuator></mujoco>
啟動仿真器后可以在右側的 Control
面板中拖動滑塊以觀察立方體運動。
2.7 定義執行器
在上面的小節中提前用到了執行器 <actuator>
,這一小節將更細致介紹如何定義執行器標簽來控制環境中的對象,你可以將執行器理解為 電機;
mujoco 提供了很多執行器工具,在其官網文檔中點擊左側 XML Reference
連接后往下拉或者搜索 actuator
即可找到可用的執行器標簽。為關節或對象添加執行器后就可以在控制對象的時候添加合適的參數,否則所有 joint 都是 free 形式。
我這里只列舉我最常用的幾個執行器,感興趣的可以查看其官網并進行實驗。
2.7.1 通用執行器 general
通用執行器允許獨立設置所有執行器組件,包括傳輸類型、激活動態、增益類型等。是一個非常靈活的執行器類型,允許用戶自定義執行器的動態特性、增益、偏置,可以實現各種類型的執行器,如直接驅動、位置伺服、速度伺服等。
這個例子實現了一個力執行器,為了讓滑塊能夠在力消失時停下來,在地面和滑塊中都添加了friction
屬性以體現摩擦力:
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/> <!-- 地面:高滑動摩擦 --><geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0" rgba="0.5 0.5 0.5 1" friction="1.5 0.3 0.05"solimp="0.9 0.95 0.001" solref="0.02 1"/><!-- 滑塊:中等摩擦 --><body name="slider" pos="0 0 0.1"><joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2"/><geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" friction="1.0 0.2 0.02"/></body></worldbody><actuator><general name="force" joint="slide_joint" ctrlrange="-1 1"/></actuator>
</mujoco>
啟動仿真器后在右側控制面板中拖拽 force
滑塊就可以給其傳入一個力,點擊 Clear all
就可以將力清空然后觀察滑塊逐漸停下來。
由于 joint 的活動范圍可以通過其屬性 range
定義,如果不想給物體添加摩擦力也可以通過限制活動范圍讓物體停下來,停下后即便有力物體也不會繼續運動
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/> <!-- 地面--><geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0" rgba="0.5 0.5 0.5 1"solimp="0.9 0.95 0.001" solref="0.02 1"/><!-- 滑塊 添加了range屬性限制活動范圍 --><body name="slider" pos="0 0 0.1"><joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/><geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" /></body></worldbody><actuator><general name="force" joint="slide_joint" ctrlrange="-1 1"/></actuator>
</mujoco>
2.7.2 位置伺服 position
位置伺服在上面的小節中已經用了一次,這個控制器就是一個位置環伺服,有點類似與 PID 一樣的控制器,所以你在滑動的時候會發現有一個明顯的回調過程,通過修改 kp
和 kv
可以體驗阻尼大小。
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/> <geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0" rgba="0.5 0.5 0.5 1"solimp="0.9 0.95 0.001" solref="0.02 1"/><body name="slider" pos="0 0 0.1"><joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/><geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" /></body></worldbody><actuator><position name="position" joint="slide_joint" kp="100" kv="10"/></actuator>
</mujoco>
2.7.3 速度伺服 velocity
既然有位置伺服那當然還有速度伺服,和位置伺服同理,速度伺服也用來調控速度到目標值
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/> <geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0" rgba="0.5 0.5 0.5 1"solimp="0.9 0.95 0.001" solref="0.02 1"/><body name="slider" pos="0 0 0.1"><joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/><geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" /></body></worldbody><actuator><velocity name="velocity" joint="slide_joint" kv="50"/></actuator>
</mujoco>
2.7.4 電機執行器 motor
電機只能輸出與關節自由度相同的力和力矩,如果你很明確這個 joint 需要輸出的是力,那么建議用這個,雖然通用執行器也可以實現相同的效果,但設置起來沒有電機執行器簡潔。
<mujoco><worldbody><light diffuse="0.5 0.5 0.5" pos="0 0 5" dir="0 0 -1"/> <geom name="ground" type="plane" size="5 5 0.1" pos="0 0 0" rgba="0.5 0.5 0.5 1"solimp="0.9 0.95 0.001" solref="0.02 1"/><body name="slider" pos="0 0 0.1"><joint name="slide_joint" type="slide" axis="1 0 0" damping="0.2" range="-2 2"/><geom type="box" size="0.2 0.1 0.1" rgba="0 0.5 0.8 1" mass="0.5" /></body></worldbody><actuator><motor name="motor" joint="slide_joint" gear="1"/></actuator>
</mujoco>
2.8 顯示坐標軸
坐標軸的顯示在仿真中非常重要,打開仿真器后展開左側工具欄中的 Rendering
標簽,通過選擇 Frame
即可選擇想要顯示的坐標軸。