using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using UnityEngine;
namespace Unity.MLAgents
{
public class MarathonSpawner : MonoBehaviour
{
[Tooltip("The MuJoCo xml file to parse")]
/**< \brief The MuJoCo xml file to parse*/
public TextAsset Xml;
public Material Material;
public PhysicMaterial PhysicMaterial;
[Tooltip("When True, UnityEngine.Time.fixedDeltaTime is set by ")]
/**< \brief When True, UnityEngine.Time.fixedDeltaTime is set by */
public bool UseXmlTimestep = true;
[Tooltip("During XML parsing, debug messages are sent to the console")]
/**< \brief During XML parsing, debug messages are sent to the console*/
public bool DebugOutput;
[Tooltip("Used for 2D MuJoCo objects (hopper, walker, etc)")]
/**< \brief Used for 2D MuJoCo objects (hopper, walker, etc)*/
public bool Force2D;
[Tooltip("The random noise applied at the start of each episode to improve training variance")]
/**< \brief The random noise applied at the start of each episode to improve training variance"*/
public float OnGenerateApplyRandom = 0.005f;
[Tooltip("The default density (Mujoco's default value is 1000)")]
/**< \brief The default density (Mujoco's default value is 1000)"*/
public float DefaultDensity = 1000f;
[Tooltip("Use to scale the power of the motors (default is 1)")]
/**< \brief Use to scale the power of the motors (default is 1)"*/
public float MotorScale = 1f;
XElement _root;
Stack _childClassStack;
Dictionary _jointXDocs;
bool _hasParsed;
bool _useWorldSpace = false;
Quaternion _orginalTransformRotation;
Vector3 _orginalTransformPosition;
public void SpawnFromXml()
{
LoadXml(Xml.text);
Parse();
}
void LoadXml(string str)
{
_root = XElement.Parse(str);
}
void DebugPrint(string str)
{
if (DebugOutput)
print(str);
}
void Parse()
{
XElement element = _root;
var name = element.Name.LocalName;
DebugPrint($"- Begin");
_jointXDocs = new Dictionary();
ParseCompilerOptions(_root);
_childClassStack = new Stack();
foreach (var attribute in element.Attributes())
{
switch (attribute.Name.LocalName)
{
case "model":
gameObject.name = attribute.Value;
break;
default:
throw new NotImplementedException();
}
}
// when using world space, geoms will be created in global space
// so setting the parent object to 0,0,0 allows us to fix that
_orginalTransformRotation = this.gameObject.transform.rotation;
_orginalTransformPosition = this.gameObject.transform.position;
this.gameObject.transform.rotation = new Quaternion();
this.gameObject.transform.position = new Vector3();
var joints = ParseBody(element.Element("worldbody"), this.gameObject);
var mujocoJoints = ParseGears(element.Element("actuator"), joints);
var mujocoSensors = ParseSensors(element.Element("sensor"), GetComponentsInChildren());
if (Material != null)
foreach (var item in GetComponentsInChildren())
{
item.material = Material;
}
if (PhysicMaterial != null)
foreach (var item in GetComponentsInChildren())
{
item.material = PhysicMaterial;
}
if (Force2D)
{
foreach (var item in GetComponentsInChildren())
item.constraints = RigidbodyConstraints.FreezePositionZ;
}
if (this.gameObject.layer != 0)
{
foreach (var item in GetComponentsInChildren())
item.gameObject.layer = this.gameObject.layer;
}
// restore positions and orientation
gameObject.transform.rotation = _orginalTransformRotation;
gameObject.transform.position = _orginalTransformPosition;
GetComponent().SetMarathonJoints(mujocoJoints);
GetComponent().SetMarathonSensors(mujocoSensors);
}
public void ApplyRandom()
{
if (OnGenerateApplyRandom != 0f)
{
float velocityScaler = 5000f;
foreach (var item in GetComponent().MarathonJoints)
{
var r = ((UnityEngine.Random.value * (OnGenerateApplyRandom * 2)) - OnGenerateApplyRandom);
// float r = 0f;
var childRb = item.Joint.GetComponent();
if (childRb != null)
{
ConfigurableJoint configurableJoint = item.Joint as ConfigurableJoint;
var t = Vector3.zero;
t.x = r * velocityScaler;
configurableJoint.targetAngularVelocity = t;
childRb.angularVelocity = t;
t = Vector3.zero;
t.x = ((UnityEngine.Random.value * (OnGenerateApplyRandom * 2)) - OnGenerateApplyRandom) * 5;
t.y = ((UnityEngine.Random.value * (OnGenerateApplyRandom * 2)) - OnGenerateApplyRandom) * 5 +
1;
t.z = ((UnityEngine.Random.value * (OnGenerateApplyRandom * 2)) - OnGenerateApplyRandom) * 5;
childRb.velocity = t;
var angX = configurableJoint.angularXDrive;
angX.positionSpring = 1f;
var scale = item.MaximumForce * Mathf.Pow(Mathf.Abs(r), 3);
angX.positionDamper = Mathf.Max(1f, scale);
angX.maximumForce = Mathf.Max(1f, scale);
configurableJoint.angularXDrive = angX;
}
}
}
}
void ParseCompilerOptions(XElement xdoc)
{
foreach (var element in xdoc.Elements("option"))
{
foreach (var attribute in element.Attributes())
{
switch (attribute.Name.LocalName)
{
case "integrator":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "iterations":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "solver":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "timestep":
if (UseXmlTimestep)
{
var timestep = (float)Convert.ToDouble(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
Time.fixedDeltaTime = timestep;
}
else
DebugPrint($"--*** IGNORING timestep=\"{attribute.Value}\" as UseXmlTimestep == false");
break;
case "gravity":
Physics.gravity = MarathonHelper.ParsePosition(attribute.Value);
break;
default:
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
}
foreach (var element in xdoc.Elements("compiler"))
{
foreach (var attribute in element.Attributes())
{
switch (attribute.Name.LocalName)
{
case "angle":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "coordinate":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
if (attribute.Value.ToLower() == "global")
_useWorldSpace = true;
else if (attribute.Value.ToLower() == "local")
_useWorldSpace = false;
break;
case "inertiafromgeom":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "settotalmass":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
default:
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
}
}
private class JointDocQueueItem
{
public XElement JointXDoc { get; set; }
public GeomItem ParentGeom { get; set; }
public GameObject ParentBody { get; set; }
}
private class GeomItem
{
public GameObject Geom;
public float? Lenght;
public float? Size;
public Vector3 Lenght3D;
public Vector3 Start;
public Vector3 End;
public List Bones;
public GeomItem()
{
Bones = new List();
}
}
List> ParseBody(XElement xdoc, GameObject parentBody, GeomItem geom = null,
GeomItem parentGeom = null, List jointDocsQueue = null)
{
var joints = new List>();
jointDocsQueue = jointDocsQueue ?? new List();
var bodies = new List();
var childClass = xdoc.Attribute("childclass");
if (childClass != null)
{
var childDoc = _root.Element("default")
?.Elements("default")
.FirstOrDefault(x => x.Attribute("class")?.Value == childClass.Value);
_childClassStack.Push(childDoc);
}
foreach (var element in xdoc.Elements("light"))
{
}
foreach (var element in xdoc.Elements("camera"))
{
}
foreach (var element in xdoc.Elements("joint"))
{
jointDocsQueue.Add(new JointDocQueueItem
{
JointXDoc = element,
ParentGeom = geom,
ParentBody = parentBody,
});
}
foreach (var element in xdoc.Elements("geom"))
{
geom = ParseGeom(element, parentBody);
if (parentGeom != null && jointDocsQueue?.Count > 0)
{
foreach (var jointDocQueueItem in jointDocsQueue)
{
var js = ParseJoint(
jointDocQueueItem.JointXDoc,
jointDocQueueItem.ParentGeom,
geom,
jointDocQueueItem.ParentBody);
if (js != null) joints.AddRange(js);
}
}
else if (parentGeom != null)
{
var fixedJoint = parentGeom.Geom.AddComponent();
fixedJoint.connectedBody = geom.Geom.GetComponent();
}
jointDocsQueue.Clear();
parentGeom = geom;
}
foreach (var element in xdoc.Elements("body"))
{
var body = new GameObject();
bodies.Add(body);
body.transform.parent = this.transform;
ApplyClassToBody(element, body, parentBody);
var newJoints = ParseBody(element, body, geom, parentGeom, jointDocsQueue);
if (newJoints != null) joints.AddRange(newJoints);
}
foreach (var item in bodies)
GameObject.Destroy(item);
if (childClass != null)
_childClassStack.Pop();
return joints;
}
void ApplyClassToBody(XElement classElement, GameObject body, GameObject parentBody)
{
foreach (var attribute in classElement.Attributes())
{
switch (attribute.Name.LocalName)
{
case "name":
body.name = attribute.Value;
break;
case "pos":
if (_useWorldSpace)
body.transform.position = MarathonHelper.ParsePosition(attribute.Value);
else
{
body.transform.position =
MarathonHelper.ParsePosition(attribute.Value) + parentBody.transform.position;
}
break;
case "quat":
if (_useWorldSpace)
body.transform.rotation = MarathonHelper.ParseQuaternion(attribute.Value);
else
{
body.transform.rotation = MarathonHelper.ParseQuaternion(attribute.Value) *
parentBody.transform.rotation;
}
break;
case "childclass":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "euler":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
default:
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
}
GeomItem ParseGeom(XElement xdoc, GameObject parent)
{
GeomItem geom = null;
if (xdoc == null)
return null;
XElement element = BuildFromClasses("geom", xdoc);
var type = element.Attribute("type")?.Value;
if (type == null)
{
DebugPrint($"--- WARNING: ParseGeom: no type found in geom. Ignoring ({element.ToString()}");
return geom;
}
float size;
float? size2 = null;
DebugPrint($"ParseGeom: Creating type:{type} name:{element.Attribute("name")?.Value}");
geom = new GeomItem();
Vector3 start;
Vector3 end;
Vector3 offset;
switch (type)
{
case "capsule":
if (element.Attribute("size")?.Value?.Split()?.Length > 1)
{
size = (float)Convert.ToDouble(element.Attribute("size")?.Value.Split()[0], System.Globalization.CultureInfo.InvariantCulture);
size2 = (float)Convert.ToDouble(element.Attribute("size")?.Value.Split()[1], System.Globalization.CultureInfo.InvariantCulture);
}
else
size = (float)Convert.ToDouble(element.Attribute("size")?.Value, System.Globalization.CultureInfo.InvariantCulture);
var fromto = element.Attribute("fromto")?.Value;
if (fromto == null)
{
var posAttribute = element.Attribute("pos")?.Value;
Vector3 centerPos = Vector3.zero;
if (posAttribute != null)
{
var rawPos = MarathonHelper.ParsePosition(posAttribute);
centerPos = centerPos - rawPos;
}
start = centerPos;
end = centerPos;
var zaxisAttribute = element.Attribute("zaxis")?.Value;
Vector3 zaxis = Vector3.up;
if (zaxisAttribute != null)
zaxis = MarathonHelper.ParseAxis(zaxisAttribute);
var zaxisScaled = zaxis * size2.Value;
start -= zaxisScaled;
end += zaxisScaled;
// end += zaxisScaled * 2;
start = MarathonHelper.RightToLeft(start);
end = MarathonHelper.RightToLeft(end);
DebugPrint($"ParseGeom: Creating type:{type} size:{size}");
}
else
{
DebugPrint($"ParseGeom: Creating type:{type} fromto:{fromto} size:{size}");
start = MarathonHelper.ParseFrom(fromto);
end = MarathonHelper.ParseTo(fromto);
}
geom.Geom = parent.CreateBetweenPoints(start, end, size, _useWorldSpace, this.gameObject);
offset = end - start;
geom.Lenght = offset.magnitude; //
geom.Size = size;
geom.Lenght3D = offset;
geom.Start = start;
geom.End = end;
break;
case "sphere":
size = (float)Convert.ToDouble(element.Attribute("size")?.Value, System.Globalization.CultureInfo.InvariantCulture);
var pos = element.Attribute("pos")?.Value ?? "0 0 0";
DebugPrint($"ParseGeom: Creating type:{type} pos:{pos} size:{size}");
geom.Geom = parent.CreateAtPoint(MarathonHelper.ParsePosition(pos), size, _useWorldSpace, this.gameObject);
geom.Size = size;
break;
default:
DebugPrint(
$"--- WARNING: ParseGeom: {type} geom is not implemented. Ignoring ({element.ToString()}");
return null;
}
var rb = geom.Geom.AddComponent();
rb.useGravity = true;
rb.SetDensity(DefaultDensity);
rb.mass = rb.mass; // ref: https://forum.unity.com/threads/rigidbody-setdensity-doesnt-work.322911/
ApplyClassToGeom(element, geom.Geom, parent);
return geom;
}
void ApplyClassToGeom(XElement classElement, GameObject geom, GameObject parentBody)
{
foreach (var attribute in classElement.Attributes())
{
switch (attribute.Name.LocalName)
{
case "name": // optional
// Name of the geom.
geom.name = attribute.Value;
break;
case "class": // optional
// Defaults class for setting unspecified attributes.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "type": // [plane, hfield, sphere, capsule, ellipsoid, cylinder, box, mesh], "sphere"
// Type of geometric shape.
// Handled in object init
break;
case "contype": // int, "1"
// This attribute and the next specify 32-bit integer bitmasks used for contact
// filtering of dynamically generated contact pairs. See Collision detection in
// the Computation chapter. Two geoms can collide if the contype of one geom is
// compatible with the conaffinity of the other geom or vice versa.
// Compatible means that the two bitmasks have a common bit set to 1.
// Note: contype="0" conaffinity="0" disables physics contacts
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "conaffinity": // int, "1"
// Bitmask for contact filtering; see contype above.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "condim": // int, "3"
// The dimensionality of the contact space for a dynamically generated contact
// pair is set to the maximum of the condim values of the two participating geoms.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "group": // int, "0"
// This attribute specifies an integer group to which the geom belongs.
// The only effect on the physics is at compile time, when body masses and inertias are
// inferred from geoms selected based on their group; see inertiagrouprange attribute of compiler.
// At runtime this attribute is used by the visualizer to enable and disable the rendering of
// entire geom groups. It can also be used as a tag for custom computations.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "size": // real(3), "0 0 0"
// Geom size parameters. The number of required parameters and their meaning depends on the
// geom type as documented under the type attribute. Here we only provide a summary.
// All required size parameters must be positive; the internal defaults correspond to invalid
// settings. Note that when a non-mesh geom type references a mesh, a geometric primitive of
// that type is fitted to the mesh. In that case the sizes are obtained from the mesh, and
// the geom size parameters are ignored. Thus the number and description of required size
// parameters in the table below only apply to geoms that do not reference meshes.
// Type Number Description
// plane 3 X half-size; Y half-size; spacing between square grid lines for rendering.
// hfield 0 The geom sizes are ignored and the height field sizes are used instead.
// sphere 1 Radius of the sphere.
// capsule 1 or 2 Radius of the capsule; half-length of the cylinder part when not using the fromto specification.
// ellipsoid 3 X radius; Y radius; Z radius.
// cylinder 1 or 2 Radius of the cylinder; half-length of the cylinder when not using the fromto specification.
// box 3 X half-size; Y half-size; Z half-size.
// mesh 0 The geom sizes are ignored and the mesh sizes are used instead.
// Handled at object init
break;
case "material": // optional
// If specified, this attribute applies a material to the geom. The material determines the visual properties of
// the geom. The only exception is color: if the rgba attribute below is different from its internal default, it takes
// precedence while the remaining material properties are still applied. Note that if the same material is referenced
// from multiple geoms (as well as sites and tendons) and the user changes some of its properties at runtime,
// these changes will take effect immediately for all model elements referencing the material. This is because the
// compiler saves the material and its properties as a separate element in mjModel, and the elements using this
// material only keep a reference to it.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "rgba": // real(4), "0.5 0.5 0.5 1"
// Instead of creating material assets and referencing them, this attribute can be used
// to set color and transparency only. This is not as flexible as the material mechanism,
// but is more convenient and is often sufficient. If the value of this attribute is
// different from the internal default, it takes precedence over the material.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "friction": //real(3), "1 0.005 0.0001"
// Contact friction parameters for dynamically generated contact pairs.
// The first number is the sliding friction, acting along both axes of the tangent plane.
// The second number is the torsional friction, acting around the contact normal.
// The third number is the rolling friction, acting around both axes of the tangent plane.
// The friction parameters for the contact pair are computed as the element-wise maximum of
// the geom-specific parameters. See also Parameters section in the Computation chapter.
float? slidingFriction = null;
float? torsionalFriction = null;
float? rollingFriction = null;
var frictionSplit = attribute.Value.Split(' ');
if (frictionSplit?.Length >= 3)
rollingFriction = (float)Convert.ToDouble(frictionSplit[2], System.Globalization.CultureInfo.InvariantCulture);
if (frictionSplit?.Length >= 2)
torsionalFriction = (float)Convert.ToDouble(frictionSplit[1], System.Globalization.CultureInfo.InvariantCulture);
if (frictionSplit?.Length >= 1)
slidingFriction = (float)Convert.ToDouble(frictionSplit[0], System.Globalization.CultureInfo.InvariantCulture);
var physicMaterial = geom.GetComponent()?.material;
physicMaterial.staticFriction = slidingFriction.Value;
if (rollingFriction.HasValue)
physicMaterial.dynamicFriction = rollingFriction.Value;
else if (torsionalFriction.HasValue)
physicMaterial.dynamicFriction = torsionalFriction.Value;
else
physicMaterial.dynamicFriction = slidingFriction.Value;
break;
case "mass": // optional
// If this attribute is specified, the density attribute below is ignored and the geom density
// is computed from the given mass, using the geom shape and the assumption of uniform density.
// The computed density is then used to obtain the geom inertia. Recall that the geom mass and
// inerta are only used during compilation, to infer the body mass and inertia if necessary.
// At runtime only the body inertial properties affect the simulation;
// the geom mass and inertia are not even saved in mjModel.
// DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
geom.GetComponent().mass = (float)Convert.ToDouble(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
break;
case "density": // "1000"
// Material density used to compute the geom mass and inertia. The computation is based on the
// geom shape and the assumption of uniform density. The internal default of 1000 is the density
// of water in SI units. This attribute is used only when the mass attribute above is unspecified.
var density = (float)Convert.ToDouble(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
var rb = geom.GetComponent();
rb.SetDensity(density);
rb.mass = rb
.mass; // ref: https://forum.unity.com/threads/rigidbody-setdensity-doesnt-work.322911/
break;
case "solmix": // "1"
// This attribute specifies the weight used for averaging of constraint solver parameters.
// Recall that the solver parameters for a dynamically generated geom pair are obtained as a
// weighted average of the geom-specific parameters.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "solref":
// Constraint solver parameters for contact simulation. See Solver parameters.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "solimp":
// Constraint solver parameters for contact simulation. See Solver parameters.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "margin": // "0"
// Distance threshold below which contacts are detected and included in the global array mjData.contact.
// This however does not mean that contact force will be generated. A contact is considered active only
// if the distance between the two geom surfaces is below margin-gap. Recall that constraint impedance
// can be a function of distance, as explained in Solver parameters. The quantity this function is
// applied to is the distance between the two geoms minus the margin plus the gap.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "gap": // "0"
// This attribute is used to enable the generation of inactive contacts, i.e. contacts that are ignored
//by the constraint solver but are included in mjData.contact for the purpose of custom computations.
// When this value is positive, geom distances between margin and margin-gap correspond to such
// inactive contacts.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "fromto": // optional
// This attribute can only be used with capsule and cylinder geoms. It provides an alternative specification
// of the geom length as well as the frame position and orientation. The six numbers are the 3D coordinates
// of one point followed by the 3D coordinates of another point. The cylinder geom (or cylinder part of the
// capsule geom) connects these two points, with the +Z axis of the geom's frame oriented from the first
// towards the second point. The frame orientation is obtained with the same procedure as the zaxis
// attribute described in Frame orientations. The frame position is in the middle between the two points.
// If this attribute is specified, the remaining position and orientation-related attributes are ignored.
// Handled at object init
break;
case "pos": // "0 0 0"
// Position of the geom frame, in local or global coordinates as determined by the coordinate
// attribute of compiler.
// Handled at object init
break;
case "hfield": // optional
// This attribute must be specified if and only if the geom type is "hfield".
// It references the height field asset to be instantiated at the position and orientation of the geom frame.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "mesh": // optional
// If the geom type is "mesh", this attribute is required. It references the mesh asset to be instantiated.
// This attribute can also be specified if the geom type corresponds to a geometric primitive, namely one
// of "sphere", "capsule", "cylinder", "ellipsoid", "box". In that case the primitive is automatically
// fitted to the mesh asset referenced here. The fitting procedure uses either the equivalent
// inertia box or the axis-aligned bounding box of the mesh, as determined by the attribute fitaabb
// of compiler. The resulting size of the fitted geom is usually what one would expect, but if not,
// it can be further adjusted with the fitscale attribute below. In the compiled mjModel the geom is
// represented as a regular geom of the specified primitive type, and there is no reference to the mesh
// used for fitting.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "quat": // "1 0 0 0"
// If the quaternion is known, this is the preferred was to specify the frame orientation because it does
// not involve conversions. Instead it is normalized to unit length and copied into mjModel during compilation.
// When a model is saved as MJCF, all frame orientations are expressed as quaternions using this attribute.
if (_useWorldSpace)
geom.transform.rotation = MarathonHelper.ParseQuaternion(attribute.Value);
else
geom.transform.localRotation =
MarathonHelper.ParseQuaternion(attribute.Value) * parentBody.transform.rotation;
break;
case "axisangle": // optional
// These are the quantities (x, y, z, a) mentioned above. The last number is the angle of rotation,
// in degrees or radians as specified by the angle attribute of compiler. The first three numbers determine
// a 3D vector which is the rotation axis. This vector is normalized to unit length during compilation,
// so the user can specify a vector of any non-zero length. Keep in mind that the rotation is right-handed;
// if the direction of the vector (x, y, z) is reversed this will result in the opposite rotation.
// Changing the sign of a can also be used to specify the opposite rotation.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "xyaxes": // optional
// The first 3 numbers are the X axis of the frame. The next 3 numbers are the Y axis of the frame,
// which is automatically made orthogonal to the X axis. The Z axis is then defined as the
// cross-product of the X and Y axes.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "zaxis": // optional
// The Z axis of the frame. The compiler finds the minimal rotation that maps the vector (0,0,1)
// into the vector specified here. This determines the X and Y axes of the frame implicitly.
// This is useful for geoms with rotational symmetry around the Z axis, as well as lights - which
// are oriented along the Z axis of their frame.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "euler": // optional
// Rotation angles around three coordinate axes. The sequence of axes around which these rotations are applied
// is determined by the eulerseq attribute of compiler and is the same for the entire model.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "fitscale": // "1"
// This attribute is used only when a primitive geometric type is being fitted to a mesh asset.
// The scale specified here is relative to the output of the automated fitting procedure. The default value
// of 1 leaves the result unchanged, a value of 2 makes all sizes of the fitted geom two times larger.
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "user":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
default:
{
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
}
}
Joint FixedJoint(GameObject parent)
{
parent.gameObject.AddComponent();
var joint = parent.GetComponent();
return joint;
}
XElement BuildFromClasses(string type, XElement xdoc)
{
XElement subClass = new XElement(type);
if (_childClassStack.Count > 0 && _childClassStack.Peek()?.Element(type) != null)
subClass = _childClassStack.Peek()?.Element(type);
var defaultClass = _root.Element("default")?.Element(type) ?? new XElement(type);
xdoc = xdoc ?? new XElement(type);
var attributes =
defaultClass.Attributes()
.Concat(subClass.Attributes())
.Concat(xdoc.Attributes())
.GroupBy(x => x.Name)
.Select(x => x.Last());
XElement element = new XElement(type, attributes);
if (element.Attribute("class") != null)
element = AddClass(type, element.Attribute("class").Value, element);
return element;
}
XElement AddClass(string type, string subClassName, XElement xdoc, XElement nestingRef = null)
{
if (nestingRef == null)
nestingRef = _root.Element("default");
foreach (var item in nestingRef.Attributes("class"))
{
if (item.Value == subClassName)
{
// found class
var subClass = nestingRef.Element(type) ?? new XElement(type);
var attributes =
xdoc.Attributes()
.Concat(subClass.Attributes())
.GroupBy(x => x.Name)
.Select(x => x.Last());
XElement element = new XElement(type, attributes);
return element;
}
}
foreach (var item in nestingRef.Elements("default"))
{
if (nestingRef != null)
xdoc = AddClass(type, subClassName, xdoc, item);
}
return xdoc;
}
List> ParseJoint(XElement xdoc, GeomItem parentGeom, GeomItem childGeom,
GameObject body)
{
_jointXDocs.Add(xdoc.Attribute("name").Value, xdoc);
var joints = new List>();
GameObject bone = null;
var childRidgedBody = childGeom.Geom.GetComponent();
var parentRidgedBody = parentGeom.Geom.GetComponent();
if (xdoc == null)
return joints;
XElement element = BuildFromClasses("joint", xdoc);
var type = element.Attribute("type")?.Value;
if (type == null)
{
DebugPrint($"--- WARNING: ParseJoint: no type found. Assuming Hinge: ({element.ToString()}");
type = "hinge";
}
Joint joint = null;
Type jointType;
string jointName = element.Attribute("name")?.Value;
switch (type)
{
case "hinge":
DebugPrint($"ParseJoint: Creating type:{type} ");
jointType = typeof(HingeJoint);
break;
case "free":
DebugPrint($"ParseJoint: Creating type:{type} ");
jointType = typeof(FixedJoint);
break;
default:
DebugPrint(
$"--- WARNING: ParseJoint: joint type '{type}' is not implemented. Ignoring ({element.ToString()}");
return joints;
}
Joint existingJoint = childGeom.Bones
.SelectMany(x => x.GetComponents())
.FirstOrDefault(y => y.connectedBody == parentRidgedBody);
if (existingJoint)
{
bone = new GameObject();
bone.transform.SetPositionAndRotation(childGeom.Geom.transform.position,
childGeom.Geom.transform.rotation);
bone.transform.localScale = childGeom.Geom.transform.localScale;
bone.transform.parent = childGeom.Geom.transform;
bone.name = jointName;
var boneRidgedBody = bone.AddComponent();
boneRidgedBody.useGravity = false;
joint = bone.AddComponent(jointType) as Joint;
existingJoint.connectedBody = boneRidgedBody;
childGeom.Bones.Add(bone);
}
else
{
joint = childGeom.Geom.AddComponent(jointType) as Joint;
if (!childGeom.Bones.Contains(childGeom.Geom))
childGeom.Bones.Add(childGeom.Geom);
}
Collider boneCollider = null;
if (bone != null)
boneCollider = CopyCollider(bone, childGeom.Geom);
joint.connectedBody = parentRidgedBody;
ApplyClassToJoint(element, joint, childGeom, body, bone ?? childGeom.Geom);
if (boneCollider != null)
Destroy(boneCollider);
// force as configurable
if (jointType == typeof(HingeJoint))
joint = ToConfigurable(joint as HingeJoint);
joints.Add(new KeyValuePair(jointName, joint));
return joints;
}
Collider CopyCollider(GameObject target, GameObject source)
{
var sourceCollider = source.GetComponent();
var sourceCapsule = sourceCollider as CapsuleCollider;
var sphereCollider = sourceCollider as SphereCollider;
Collider targetCollider = null;
if (sourceCapsule != null)
{
var targetCapsule = target.AddComponent();
targetCollider = targetCapsule as Collider;
targetCapsule.center = sourceCapsule.center;
targetCapsule.radius = sourceCapsule.radius;
targetCapsule.height = sourceCapsule.height;
targetCapsule.direction = sourceCapsule.direction;
}
else if (sphereCollider != null)
{
var targetSphere = target.AddComponent();
targetCollider = targetSphere as Collider;
targetSphere.center = sphereCollider.center;
targetSphere.radius = sphereCollider.radius;
}
else
throw new NotImplementedException();
if (sourceCollider != null)
{
targetCollider.isTrigger = sourceCollider.isTrigger;
targetCollider.material = sourceCollider.material;
}
return targetCollider;
}
void ApplyClassToJoint(XElement classElement, Joint joint, GeomItem baseGeom, GameObject body, GameObject bone)
{
HingeJoint hingeJoint = joint as HingeJoint;
FixedJoint fixedJoint = joint as FixedJoint;
ConfigurableJoint configurableJoint = joint as ConfigurableJoint;
JointSpring spring = hingeJoint?.spring ?? new JointSpring();
JointMotor motor = hingeJoint?.motor ?? new JointMotor();
JointLimits limits = hingeJoint?.limits ?? new JointLimits();
Vector3 jointOffset = Vector3.zero;
foreach (var attribute in classElement.Attributes())
{
switch (attribute.Name.LocalName)
{
case "armature":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "damping":
spring.damper = (float)Convert.ToDouble(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
break;
case "limited":
if (hingeJoint != null)
hingeJoint.useLimits = Convert.ToBoolean(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
break;
case "axis":
var axis = MarathonHelper.ParseAxis(attribute.Value);
axis = baseGeom.Geom.transform.InverseTransformDirection(axis);
joint.axis = axis;
break;
case "name":
if (bone != null && string.IsNullOrEmpty(bone.name))
bone.name = attribute.Value;
break;
case "pos":
jointOffset = MarathonHelper.ParsePosition(attribute.Value);
break;
case "range":
if (hingeJoint != null)
{
limits.min = MarathonHelper.ParseGetMin(attribute.Value);
limits.max = MarathonHelper.ParseGetMax(attribute.Value);
limits.bounceMinVelocity = 0f;
hingeJoint.useLimits = true;
}
else if (configurableJoint != null)
{
var low = configurableJoint.lowAngularXLimit;
low.limit = MarathonHelper.ParseGetMin(attribute.Value);
configurableJoint.lowAngularXLimit = low;
var high = configurableJoint.highAngularXLimit;
high.limit = MarathonHelper.ParseGetMax(attribute.Value);
configurableJoint.highAngularXLimit = high;
}
break;
case "class":
break;
case "type":
// NOTE: handle in setup
break;
case "solimplimit":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "solreflimit":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "stiffness":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
case "margin":
DebugPrint($"{name} {attribute.Name.LocalName}={attribute.Value}");
break;
default:
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
if (_useWorldSpace)
{
var jointPos = jointOffset;
var localAxis = joint.transform.InverseTransformPoint(jointPos);
joint.anchor = localAxis;
}
else
{
var jointPos = body.transform.position;
jointPos -= bone.transform.position;
jointPos += jointOffset;
var localAxis = bone.transform.InverseTransformDirection(jointPos);
joint.anchor = localAxis;
}
if (hingeJoint != null)
{
hingeJoint.spring = spring;
hingeJoint.motor = motor;
hingeJoint.limits = limits;
}
}
static Vector3 GetLocalOrthoDirection(Transform transform, Vector3 worldDir)
{
worldDir = worldDir.normalized;
float dotX = Vector3.Dot(worldDir, transform.right);
float dotY = Vector3.Dot(worldDir, transform.up);
float dotZ = Vector3.Dot(worldDir, transform.forward);
float absDotX = Mathf.Abs(dotX);
float absDotY = Mathf.Abs(dotY);
float absDotZ = Mathf.Abs(dotZ);
Vector3 orthoDirection = Vector3.right;
if (absDotY > absDotX && absDotY > absDotZ) orthoDirection = Vector3.up;
if (absDotZ > absDotX && absDotZ > absDotY) orthoDirection = Vector3.forward;
if (Vector3.Dot(worldDir, transform.rotation * orthoDirection) < 0f) orthoDirection = -orthoDirection;
return orthoDirection;
}
List ParseGears(XElement xdoc, List> joints)
{
var mujocoJoints = new List();
var name = "motor";
var elements = xdoc?.Elements(name);
if (elements == null)
return mujocoJoints;
foreach (var element in elements)
{
mujocoJoints.AddRange(ParseGear(element, joints));
}
foreach (var mujocoJoint in mujocoJoints)
{
mujocoJoint.TrueBase = FindTrueBase(mujocoJoint.Joint, mujocoJoints);
mujocoJoint.TrueTarget = FindTrueTarget(mujocoJoint.Joint, mujocoJoints);
mujocoJoint.MaximumForce = (mujocoJoint.Joint as ConfigurableJoint).angularXDrive.maximumForce;
Vector3 worldSwingAxis = mujocoJoint.Joint.axis;
Vector3 axis2 = GetLocalOrthoDirection(mujocoJoint.TrueTarget.transform, worldSwingAxis);
Vector3 twistAxis = GetLocalOrthoDirection(mujocoJoint.TrueTarget.transform,
mujocoJoint.TrueTarget.transform.position - mujocoJoint.TrueBase.connectedBody.transform.position);
Vector3 secondaryAxis = Vector3.Cross(axis2, twistAxis);
(mujocoJoint.Joint as ConfigurableJoint).secondaryAxis = secondaryAxis;
}
return mujocoJoints;
}
ConfigurableJoint FindTrueBase(Joint joint, List mJoints)
{
ConfigurableJoint configurableJoint = joint as ConfigurableJoint;
var rb = configurableJoint.GetComponent();
if (rb.useGravity)
return configurableJoint;
ConfigurableJoint parentRb = mJoints
.Select(x => x.Joint)
.First(x => x.connectedBody == rb)
as ConfigurableJoint;
return FindTrueBase(parentRb, mJoints);
}
Transform FindTrueTarget(Joint joint, List mJoints)
{
var targetRB = joint.connectedBody;
var rb = joint.GetComponent();
if (targetRB.useGravity)
return targetRB.transform;
var target = targetRB.GetComponent();
if (target == null)
return targetRB.transform;
return FindTrueTarget(target, mJoints);
}
List ParseGear(XElement xdoc, List> joints)
{
var mujocoJoints = new List();
XElement element = BuildFromClasses("gear", xdoc);
string jointName = element.Attribute("joint")?.Value;
if (jointName == null)
{
DebugPrint($"--- WARNING: ParseGears: no jointName found. Ignoring ({element.ToString()}");
return mujocoJoints;
}
var matches = joints.Where(x => x.Key.ToLowerInvariant() == jointName.ToLowerInvariant())
?.Select(x => x.Value);
if (matches == null)
{
DebugPrint(
$"--- ERROR: ParseGears: joint:'{jointName}' was not found in joints. Ignoring ({element.ToString()}");
return mujocoJoints;
}
foreach (Joint joint in matches)
{
HingeJoint hingeJoint = joint as HingeJoint;
ConfigurableJoint configurableJoint = joint as ConfigurableJoint;
JointSpring spring = new JointSpring();
JointMotor motor = new JointMotor();
if (hingeJoint != null)
{
spring = hingeJoint.spring;
hingeJoint.useSpring = false;
hingeJoint.useMotor = true;
motor = hingeJoint.motor;
motor.freeSpin = true;
}
if (configurableJoint != null)
{
configurableJoint.rotationDriveMode = RotationDriveMode.XYAndZ;
}
var mujocoJoint = new MarathonJoint
{
Joint = joint,
JointName = jointName,
};
ApplyClassToGear(element, joint, mujocoJoint);
mujocoJoints.Add(mujocoJoint);
}
return mujocoJoints;
}
void ApplyClassToGear(XElement classElement, Joint joint, MarathonJoint mujocoJoint)
{
HingeJoint hingeJoint = joint as HingeJoint;
FixedJoint fixedJoint = joint as FixedJoint;
ConfigurableJoint configurableJoint = joint as ConfigurableJoint;
JointSpring spring = hingeJoint?.spring ?? new JointSpring();
JointMotor motor = hingeJoint?.motor ?? new JointMotor();
JointLimits limits = hingeJoint?.limits ?? new JointLimits();
var angularXDrive = configurableJoint?.angularXDrive ?? new JointDrive();
foreach (var attribute in classElement.Attributes())
{
switch (attribute.Name.LocalName)
{
case "joint":
break;
case "ctrllimited":
var ctrlLimited = Convert.ToBoolean(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
mujocoJoint.CtrlLimited = ctrlLimited;
break;
case "ctrlrange":
var ctrlRange = MarathonHelper.ParseVector2(attribute.Value);
mujocoJoint.CtrlRange = ctrlRange;
break;
case "gear":
var gear = (float)Convert.ToDouble(attribute.Value, System.Globalization.CultureInfo.InvariantCulture);
gear *= MotorScale;
//var gear = 200;
mujocoJoint.Gear = gear;
spring.spring = gear;
motor.force = gear;
angularXDrive.maximumForce = gear;
angularXDrive.positionDamper = 1;
angularXDrive.positionSpring = 1;
break;
case "name":
var objName = attribute.Value;
mujocoJoint.Name = objName;
break;
default:
DebugPrint($"*** MISSING --> {name}.{attribute.Name.LocalName}");
throw new NotImplementedException(attribute.Name.LocalName);
#pragma warning disable
break;
}
}
if (hingeJoint != null)
{
hingeJoint.spring = spring;
hingeJoint.motor = motor;
hingeJoint.limits = limits;
}
if (configurableJoint != null)
{
configurableJoint.angularXDrive = angularXDrive;
}
}
List ParseSensors(XElement xdoc, IEnumerable colliders)
{
var mujocoSensors = new List();
var name = "touch";
var elements = xdoc?.Elements(name);
if (elements == null)
return mujocoSensors;
foreach (var element in elements)
{
var mujocoSensor = new MarathonSensor
{
Name = element.Attribute("name")?.Value,
SiteName = element.Attribute("site")?.Value,
};
var match = colliders
.Where(x => x.name == mujocoSensor.SiteName)
.FirstOrDefault();
if (match != null)
mujocoSensor.SiteObject = match;
else
throw new NotImplementedException();
mujocoSensors.Add(mujocoSensor);
}
return mujocoSensors;
}
public static Joint ToConfigurable(HingeJoint hingeJoint)
{
if (hingeJoint.useMotor)
{
throw new NotImplementedException();
}
ConfigurableJoint configurableJoint = hingeJoint.gameObject.AddComponent();
configurableJoint.anchor = hingeJoint.anchor;
configurableJoint.autoConfigureConnectedAnchor = hingeJoint.autoConfigureConnectedAnchor;
configurableJoint.axis = new Vector3(0 - hingeJoint.axis.x, 0 - hingeJoint.axis.y, 0 - hingeJoint.axis.z);
configurableJoint.breakForce = hingeJoint.breakForce;
configurableJoint.breakTorque = hingeJoint.breakTorque;
configurableJoint.connectedAnchor = hingeJoint.connectedAnchor;
configurableJoint.connectedBody = hingeJoint.connectedBody;
configurableJoint.enableCollision = hingeJoint.enableCollision;
configurableJoint.secondaryAxis = Vector3.zero;
configurableJoint.xMotion = ConfigurableJointMotion.Locked;
configurableJoint.yMotion = ConfigurableJointMotion.Locked;
configurableJoint.zMotion = ConfigurableJointMotion.Locked;
configurableJoint.angularXMotion =
hingeJoint.useLimits ? ConfigurableJointMotion.Limited : ConfigurableJointMotion.Free;
configurableJoint.angularYMotion = ConfigurableJointMotion.Locked;
configurableJoint.angularZMotion = ConfigurableJointMotion.Locked;
SoftJointLimit limit = new SoftJointLimit();
limit.limit = hingeJoint.limits.max;
limit.bounciness = hingeJoint.limits.bounciness;
configurableJoint.highAngularXLimit = limit;
limit = new SoftJointLimit();
limit.limit = hingeJoint.limits.min;
limit.bounciness = hingeJoint.limits.bounciness;
configurableJoint.lowAngularXLimit = limit;
SoftJointLimitSpring limitSpring = new SoftJointLimitSpring();
limitSpring.damper = hingeJoint.useSpring ? hingeJoint.spring.damper : 0f;
limitSpring.spring = hingeJoint.useSpring ? hingeJoint.spring.spring : 0f;
configurableJoint.angularXLimitSpring = limitSpring;
GameObject.DestroyImmediate(hingeJoint);
return configurableJoint;
}
}
}