Files
Mujoco_WASM/unity/Runtime/Importer/MjcfImporter.cs
T

451 lines
16 KiB
C#

// Copyright 2019 DeepMind Technologies Limited
//
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using UnityEngine;
namespace Mujoco {
// API for importing Mujoco XML files into Unity scenes.
public class MjcfImporter {
// Any materials created at runtime will only be automatically cleaned up if no other objects
// reference it AND Unity changes scenes. In long-running worlds with no scene resets, this means
// those materials are never cleaned even after their MjBodies are destroyed, so we cache a
// single shared material to use for all generated meshes.
public static Material DefaultMujocoMaterial {
get {
if (_DefaultMujocoMaterial == null) {
_DefaultMujocoMaterial = new Material(Shader.Find("Standard"));
}
return _DefaultMujocoMaterial;
}
set {
_DefaultMujocoMaterial = value;
}
}
// Modifiers that change the settings of parsed nodes. They're limited to the scope of ParseRoot
// method.
protected MjXmlModifiers _modifiers;
private static Material _DefaultMujocoMaterial;
private const string _angleTypeDegree = "degree";
private int _numGeneratedNames = 0;
// A list of callbacks on how to handle custom node types.
private IDictionary<string, Action<XmlElement, GameObject>> _customNodeHandlers =
new Dictionary<string, Action<XmlElement, GameObject>>();
public void AddNodeHandler(string name, Action<XmlElement, GameObject> handler) {
_customNodeHandlers[name] = handler;
}
public MjcfImporter() {}
public GameObject ImportString(string mjcfString, string name = null) {
var mjcfXml = new XmlDocument();
mjcfXml.LoadXml(mjcfString);
return ImportXml(mjcfXml, name);
}
// A generic version of CreateGameObjectWithUniqueName.
public GameObject CreateGameObjectWithUniqueName<T>(
GameObject parentObject, XmlElement parentNode) where T : MjComponent {
return CreateGameObjectWithUniqueName(parentObject, parentNode, typeof(T));
}
public string GenerateUniqueName(XmlElement parentNode) {
var name = string.Empty;
if (parentNode.HasAttribute("name")) {
name = parentNode.GetAttribute("name");
}
if (string.IsNullOrEmpty(name)) {
name = parentNode.Name + "_" + _numGeneratedNames++;
}
return name;
}
public unsafe GameObject ImportXml(XmlDocument mjcfXml, string name = null) {
// Reset the numbering of anonymous nodes to ensure deterministic naming of loaded scenes.
_numGeneratedNames = 0;
_modifiers = new MjXmlModifiers(mjcfXml);
ConfigureAngleType(mjcfXml);
// We're adopting the approach of building the scene hierarchy as we parse the Mjcf.
// Should we encounter any error, we'll terminate and clean it immediately.
// Having a single root object for that will be very helpful, since we only need to delete that
// object.
var sceneRoot = new GameObject(name ?? "default");
// We'll use Exceptions as a way to communicate errors encountered during parsing.
try {
ParseRoot(sceneRoot, mjcfXml.SelectSingleNode("/mujoco") as XmlElement);
} catch (Exception ex) {
// We consider any error as critical, and end the import process immediately, cleaning up
// what we have already built.
Debug.LogException(ex);
GameObject.DestroyImmediate(sceneRoot);
throw ex;
} finally {
_modifiers = null;
}
return sceneRoot;
}
// Creates a new GameObject, assigns it a unique name, and attaches a component of the specified
// type. If the requested component type is abstract, the method will attempt to instantiate one
// of its implementations that passes the parser test.
protected GameObject CreateGameObjectWithUniqueName(
GameObject parentObject, XmlElement parentNode, Type componentType) {
var name = GenerateUniqueName(parentNode);
var gameObject = new GameObject(name);
gameObject.transform.parent = parentObject.transform;
var component = gameObject.AddComponent(componentType) as MjComponent;
component.ParseMjcf(parentNode);
return gameObject;
}
protected void ConfigureAngleType(XmlDocument mjcfXml) {
var compilerNode = mjcfXml.SelectSingleNode("/mujoco/compiler") as XmlElement;
if (compilerNode != null) {
// Parse the angle type.
var angleSetting = compilerNode.GetStringAttribute("angle", defaultValue: _angleTypeDegree);
MjSceneImportSettings.AnglesInDegrees = angleSetting == _angleTypeDegree;
}
}
protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) {
if (mujocoNode.SelectSingleNode("option") != null
|| mujocoNode.SelectSingleNode("size") != null
|| mujocoNode.SelectSingleNode("custom") != null) {
var globalsObject = CreateGameObjectInParent("Global Settings", rootObject);
var settingsComponent = globalsObject.AddComponent<MjGlobalSettings>();
settingsComponent.ParseGlobalMjcfSections(mujocoNode);
}
// This makes references to assets.
var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement;
ParseBodyChildren(rootObject, worldBodyNode);
// This section references bodies, must be parsed after worldbody.
var excludeNode = mujocoNode.SelectSingleNode("contact") as XmlElement;
if (excludeNode != null) {
var excludesParentObject = CreateGameObjectInParent("excludes", rootObject);
foreach (var child in excludeNode.OfType<XmlElement>()) {
if (child.Name != "exclude") {
Debug.LogWarning(
$"Only 'exclude' is supported - {child.Name} isn't supported yet.",
rootObject);
} else {
_modifiers.ApplyModifiersToElement(child);
CreateGameObjectWithUniqueName<MjExclude>(excludesParentObject, child);
}
}
}
// This section references joints/sites/geoms, must be parsed after worldbody.
var tendonNode = mujocoNode.SelectSingleNode("tendon") as XmlElement;
if (tendonNode != null) {
var tendonsParentObject = CreateGameObjectInParent("tendons", rootObject);
foreach (var child in tendonNode.OfType<XmlElement>()) {
_modifiers.ApplyModifiersToElement(child, elementName: "tendon");
if (child.Name == "fixed") {
CreateGameObjectWithUniqueName<MjFixedTendon>(tendonsParentObject, child);
} else if (child.Name == "spatial") {
CreateGameObjectWithUniqueName<MjSpatialTendon>(tendonsParentObject, child);
} else {
Debug.Log($"Tendon type {child.Name} not supported.");
}
}
}
// This section references worldbody elements + tendons, must be parsed after them.
var equalityNode = mujocoNode.SelectSingleNode("equality") as XmlElement;
if (equalityNode != null) {
var equalitiesParentObject = CreateGameObjectInParent("equality constraints", rootObject);
foreach (var child in equalityNode.OfType<XmlElement>()) {
var equalityType = ParseEqualityType(child);
_modifiers.ApplyModifiersToElement(child);
CreateGameObjectWithUniqueName(equalitiesParentObject, child, equalityType);
}
}
// This section references joints and tendons, must be parsed after worldbody and tendon.
var actuatorNode = mujocoNode.SelectSingleNode("actuator") as XmlElement;
if (actuatorNode != null) {
var actuatorsParentObject = CreateGameObjectInParent("actuators", rootObject);
foreach (var child in actuatorNode.OfType<XmlElement>()) {
_modifiers.ApplyModifiersToElement(child);
CreateGameObjectWithUniqueName<MjActuator>(actuatorsParentObject, child);
}
}
// This section references tendons, actuators and worldbody elements, must be parsed last.
var sensorNode = mujocoNode.SelectSingleNode("sensor") as XmlElement;
if (sensorNode != null) {
var sensorParentObject = CreateGameObjectInParent("sensors", rootObject);
foreach (var child in sensorNode.OfType<XmlElement>()) {
_modifiers.ApplyModifiersToElement(child);
var sensorType = ParseSensorType(child);
if (sensorType != null) {
CreateGameObjectWithUniqueName(sensorParentObject, child, sensorType);
}
}
}
}
protected virtual void ParseGeom(GameObject parentObject, XmlElement child) {
var gameObject = CreateGameObjectWithUniqueName<MjGeom>(parentObject, child);
// Add the visuals.
gameObject.AddComponent<MjMeshFilter>();
var renderer = gameObject.AddComponent<MeshRenderer>();
renderer.sharedMaterial = DefaultMujocoMaterial;
}
private GameObject CreateGameObjectInParent(string name, GameObject parentObject) {
var createdObject = new GameObject(name);
createdObject.transform.parent = parentObject.transform;
return createdObject;
}
private void ParseBodyChildren(GameObject parentObject, XmlElement parentNode) {
foreach (var child in parentNode.Cast<XmlNode>().OfType<XmlElement>()) {
_modifiers.ApplyModifiersToElement(child);
if (_customNodeHandlers.TryGetValue(child.Name, out var handler)) {
handler?.Invoke(child, parentObject);
} else {
ParseBodyChild(child, parentObject);
}
}
}
// Called by ParseBodyChildren for each XML node, overridable by inheriting classes.
private void ParseBodyChild(XmlElement child, GameObject parentObject) {
switch (child.Name) {
case "geom": {
// this one's different because it's overwritten in MjImporterWithAssets.
ParseGeom(parentObject, child);
break;
}
case "inertial": {
CreateGameObjectWithUniqueName<MjInertial>(parentObject, child);
break;
}
case "body": {
GameObject childObject = null;
const string mocapAttribute = "mocap";
if (child.HasAttribute(mocapAttribute)) {
var mocapValueStr = child.GetAttribute(mocapAttribute);
var mocapValue = bool.Parse(mocapValueStr);
if (mocapValue) {
childObject = CreateGameObjectWithUniqueName<MjMocapBody>(parentObject, child);
}
}
if (childObject == null) {
childObject = CreateGameObjectWithUniqueName<MjBody>(parentObject, child);
}
ParseBodyChildren(childObject, child);
break;
}
case "joint": {
var jointType = ParseJointType(child);
CreateGameObjectWithUniqueName(parentObject, child, jointType);
break;
}
case "freejoint": {
CreateGameObjectWithUniqueName<MjFreeJoint>(parentObject, child);
break;
}
case "site": {
CreateGameObjectWithUniqueName<MjSite>(parentObject, child);
break;
}
case "camera": {
CreateGameObjectWithCamera(parentObject, child);
break;
}
case "plugin": {
Debug.Log($"Plugin elements are only partially supported.");
break;
}
default: {
Debug.Log($"The importer does not yet support tags <{child.Name}>.");
break;
}
}
}
private static Type ParseEqualityType(XmlElement node) {
Type equalityType = null;
switch (node.Name) {
case "connect":
equalityType = typeof(MjConnect);
break;
case "weld":
equalityType = typeof(MjWeld);
break;
case "joint":
equalityType = typeof(MjJointConstraint);
break;
case "tendon":
equalityType = typeof(MjTendonConstraint);
break;
default:
Debug.Log($"The importer does not yet support equality <{node.Name}>.");
break;
}
return equalityType;
}
private static Type ParseSensorType(XmlElement node) {
Type sensorType = null;
switch (node.Name) {
case "actuatorpos":
case "actuatorvel":
case "actuatorfrc":
sensorType = typeof(MjActuatorScalarSensor);
break;
case "subtreecom":
case "subtreelinvel":
case "subtreeangmom":
sensorType = typeof(MjBodyVectorSensor);
break;
case "jointpos":
case "jointvel":
case "jointlimitpos":
case "jointlimitvel":
case "jointlimitfrc":
sensorType = typeof(MjJointScalarSensor);
break;
case "touch":
case "rangefinder":
sensorType = typeof(MjSiteScalarSensor);
break;
case "accelerometer":
case "velocimeter":
case "force":
case "torque":
case "gyro":
case "magnetometer":
sensorType = typeof(MjSiteVectorSensor);
break;
case "framequat":
switch (node.GetAttribute("objtype")) {
case "body":
case "xbody":
sensorType = typeof(MjBodyQuaternionSensor);
break;
case "geom":
sensorType = typeof(MjGeomQuaternionSensor);
break;
case "site":
sensorType = typeof(MjSiteQuaternionSensor);
break;
default:
Debug.Log($"camera sensors unsupported <{node.Name}>.");
break;
}
break;
case "framepos":
case "framexaxis":
case "frameyaxis":
case "framezaxis":
case "framelinvel":
case "frameangvel":
case "framelinacc":
case "frameangacc":
switch (node.GetAttribute("objtype")) {
case "body":
case "xbody":
sensorType = typeof(MjBodyVectorSensor);
break;
case "geom":
sensorType = typeof(MjGeomVectorSensor);
break;
case "site":
sensorType = typeof(MjSiteVectorSensor);
break;
default:
Debug.Log($"camera sensors unsupported <{node.Name}>.");
break;
}
break;
case "user":
sensorType = typeof(MjUserSensor);
break;
default:
Debug.Log($"The importer does not yet support sensor <{node.Name}>.");
break;
}
return sensorType;
}
private Type ParseJointType(XmlElement parentNode) {
Type jointType = null;
switch (parentNode.GetStringAttribute("type", defaultValue: string.Empty)) {
case "hinge":
jointType = typeof(MjHingeJoint);
break;
case "ball":
jointType = typeof(MjBallJoint);
break;
case "slide":
jointType = typeof(MjSlideJoint);
break;
case "free":
jointType = typeof(MjFreeJoint);
break;
default:
// According to http://mujoco.org/book/XMLreference.html#joint, the default type for a joint
// is Hinge.
jointType = typeof(MjHingeJoint);
break;
}
return jointType;
}
private GameObject CreateGameObjectWithCamera(
GameObject parentObject, XmlElement parentNode) {
var name = GenerateUniqueName(parentNode);
var gameObject = new GameObject(name);
gameObject.transform.parent = parentObject.transform;
MjEngineTool.ParseTransformMjcf(parentNode, gameObject.transform);
if (parentNode.GetStringAttribute("mode") == "targetbody") {
// Look at the parent body:
gameObject.transform.localRotation = Quaternion.LookRotation(
gameObject.transform.InverseTransformPoint(parentObject.transform.position));
} else {
// MuJoCo's camera convention is to look down the negative z, which post-swizzle became
// negative y, while Unity looks down positive z:
gameObject.transform.localRotation *=
Quaternion.FromToRotation(Vector3.forward, -Vector3.up);
}
var camera = gameObject.AddComponent<Camera>();
camera.fieldOfView = parentNode.GetFloatAttribute("fovy", defaultValue: 45.0f);
camera.nearClipPlane = 0.01f; // MuJoCo default, TODO(etom): get from visual/map/znear
return gameObject;
}
}
}