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 <option timestep=xxx>")] /**< \brief When True, UnityEngine.Time.fixedDeltaTime is set by <option timestep=xxx>*/ 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<XElement> _childClassStack; Dictionary<string, XElement> _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<string, XElement>(); ParseCompilerOptions(_root); _childClassStack = new Stack<XElement>(); 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<Collider>()); if (Material != null) foreach (var item in GetComponentsInChildren<Renderer>()) { item.material = Material; } if (PhysicMaterial != null) foreach (var item in GetComponentsInChildren<Collider>()) { item.material = PhysicMaterial; } if (Force2D) { foreach (var item in GetComponentsInChildren<Rigidbody>()) item.constraints = RigidbodyConstraints.FreezePositionZ; } if (this.gameObject.layer != 0) { foreach (var item in GetComponentsInChildren<Collider>()) item.gameObject.layer = this.gameObject.layer; } // restore positions and orientation gameObject.transform.rotation = _orginalTransformRotation; gameObject.transform.position = _orginalTransformPosition; GetComponent<MarathonAgent>().SetMarathonJoints(mujocoJoints); GetComponent<MarathonAgent>().SetMarathonSensors(mujocoSensors); } public void ApplyRandom() { if (OnGenerateApplyRandom != 0f) { float velocityScaler = 5000f; foreach (var item in GetComponent<MarathonAgent>().MarathonJoints) { var r = ((UnityEngine.Random.value * (OnGenerateApplyRandom * 2)) - OnGenerateApplyRandom); // float r = 0f; var childRb = item.Joint.GetComponent<Rigidbody>(); 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<GameObject> Bones; public GeomItem() { Bones = new List<GameObject>(); } } List<KeyValuePair<string, Joint>> ParseBody(XElement xdoc, GameObject parentBody, GeomItem geom = null, GeomItem parentGeom = null, List<JointDocQueueItem> jointDocsQueue = null) { var joints = new List<KeyValuePair<string, Joint>>(); jointDocsQueue = jointDocsQueue ?? new List<JointDocQueueItem>(); var bodies = new List<GameObject>(); 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>(); fixedJoint.connectedBody = geom.Geom.GetComponent<Rigidbody>(); } 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<Rigidbody>(); 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<Collider>()?.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<Rigidbody>().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<Rigidbody>(); 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<FixedJoint>(); var joint = parent.GetComponent<Joint>(); 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<KeyValuePair<string, Joint>> ParseJoint(XElement xdoc, GeomItem parentGeom, GeomItem childGeom, GameObject body) { _jointXDocs.Add(xdoc.Attribute("name").Value, xdoc); var joints = new List<KeyValuePair<string, Joint>>(); GameObject bone = null; var childRidgedBody = childGeom.Geom.GetComponent<Rigidbody>(); var parentRidgedBody = parentGeom.Geom.GetComponent<Rigidbody>(); 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<Joint>()) .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<Rigidbody>(); 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<string, Joint>(jointName, joint)); return joints; } Collider CopyCollider(GameObject target, GameObject source) { var sourceCollider = source.GetComponent<Collider>(); var sourceCapsule = sourceCollider as CapsuleCollider; var sphereCollider = sourceCollider as SphereCollider; Collider targetCollider = null; if (sourceCapsule != null) { var targetCapsule = target.AddComponent<CapsuleCollider>(); 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<SphereCollider>(); 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<MarathonJoint> ParseGears(XElement xdoc, List<KeyValuePair<string, Joint>> joints) { var mujocoJoints = new List<MarathonJoint>(); 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<MarathonJoint> mJoints) { ConfigurableJoint configurableJoint = joint as ConfigurableJoint; var rb = configurableJoint.GetComponent<Rigidbody>(); 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<MarathonJoint> mJoints) { var targetRB = joint.connectedBody; var rb = joint.GetComponent<Rigidbody>(); if (targetRB.useGravity) return targetRB.transform; var target = targetRB.GetComponent<ConfigurableJoint>(); if (target == null) return targetRB.transform; return FindTrueTarget(target, mJoints); } List<MarathonJoint> ParseGear(XElement xdoc, List<KeyValuePair<string, Joint>> joints) { var mujocoJoints = new List<MarathonJoint>(); 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<MarathonSensor> ParseSensors(XElement xdoc, IEnumerable<Collider> colliders) { var mujocoSensors = new List<MarathonSensor>(); 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>(); 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; } } }