diff --git a/.gitignore b/.gitignore index ba97a7c9b2..fdedf10e84 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.o *.so *.so.* +*.dll.meta # Exclude editor config .vscode/ diff --git a/unity/Editor/Components/MjFlexDeformableEditor.cs b/unity/Editor/Components/MjFlexDeformableEditor.cs new file mode 100644 index 0000000000..cccfbfacad --- /dev/null +++ b/unity/Editor/Components/MjFlexDeformableEditor.cs @@ -0,0 +1,67 @@ +using UnityEngine; +using UnityEditor; + +namespace Mujoco { + [CustomEditor(typeof(MjFlexDeformable))] + public class MjFlexDeformableEditor : Editor { + public override void OnInspectorGUI() { + serializedObject.Update(); + + // To have properly rendered, dynamic edge/elasticity/contact properties with dropdowns + // without manually creating the dropdown, HideInInspector in the main class doesn't work, + // would end up needing to manually create the dropdown views. That would let us simply call + // the default inspector for the other fields, but instead let's just add the boilerplate + // them. + + EditorGUILayout.PropertyField(serializedObject.FindProperty("FlexName"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Dim"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Radius"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Body"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Vertex"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Texcoord"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Element"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Flatskin"), true); + EditorGUILayout.PropertyField(serializedObject.FindProperty("Group"), true); + var component = (MjFlexDeformable)target; + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("", GUI.skin.horizontalSlider); + + // Draw our hidden configuration sections + SerializedProperty configureEdge = serializedObject.FindProperty("ConfigureEdge"); + SerializedProperty configureContact = serializedObject.FindProperty("ConfigureContact"); + SerializedProperty configureElasticity = serializedObject.FindProperty("ConfigureElasticity"); + + // Edge Configuration + EditorGUILayout.PropertyField(configureEdge); + if (configureEdge.boolValue) { + EditorGUI.indentLevel++; + var edge = serializedObject.FindProperty("Edge"); + EditorGUILayout.PropertyField(edge, true); + EditorGUI.indentLevel--; + EditorGUILayout.Space(5); + } + + // Contact Configuration + EditorGUILayout.PropertyField(configureContact); + if (configureContact.boolValue) { + EditorGUI.indentLevel++; + var contact = serializedObject.FindProperty("Contact"); + EditorGUILayout.PropertyField(contact, true); + EditorGUI.indentLevel--; + EditorGUILayout.Space(5); + } + + // Elasticity Configuration + EditorGUILayout.PropertyField(configureElasticity); + if (configureElasticity.boolValue) { + EditorGUI.indentLevel++; + var elasticity = serializedObject.FindProperty("Elasticity"); + EditorGUILayout.PropertyField(elasticity, true); + EditorGUI.indentLevel--; + } + + serializedObject.ApplyModifiedProperties(); + } + } +} \ No newline at end of file diff --git a/unity/Editor/Components/MjFlexDeformableEditor.cs.meta b/unity/Editor/Components/MjFlexDeformableEditor.cs.meta new file mode 100644 index 0000000000..4b010e379c --- /dev/null +++ b/unity/Editor/Components/MjFlexDeformableEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 75bfd7bf0cf79c64a82f60b7de03a353 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Custom.meta b/unity/Runtime/Components/Custom.meta new file mode 100644 index 0000000000..d8e39f6a97 --- /dev/null +++ b/unity/Runtime/Components/Custom.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 41690b7ef36105d46b836cb01418a374 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Custom/MjCustom.cs b/unity/Runtime/Components/Custom/MjCustom.cs new file mode 100644 index 0000000000..bb0bc18608 --- /dev/null +++ b/unity/Runtime/Components/Custom/MjCustom.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using UnityEngine; +using UnityEngine.Serialization; + +namespace Mujoco { +public class MjCustom : MonoBehaviour { + + [SerializeField] + private List texts; + + [SerializeField] + private List numerics; + + [SerializeField] + private List tuples; + + public static MjCustom Instance { + get + { + if (_instance == null) { + var instances = FindObjectsOfType(); + if (instances.Length > 1) { + throw new InvalidOperationException( + "Only one MjCustom instance is allowed - please resolve manually."); + } else if (instances.Length == 1) { + _instance = instances[0]; + } + } + + return _instance; + } + } + + private static MjCustom _instance = null; + + public static bool InstanceExists { get => _instance != null; } + + public void Awake() { + if (_instance == null) { + _instance = this; + } else if (_instance != this) { + throw new InvalidOperationException( + "At most one MjCustom should be present."); + } + } + + + public void ParseCustom(XmlElement child) { + switch (child.Name) { + case "numeric": + numerics ??= new List(); + var numeric = new MjNumeric(); + numeric.Parse(child); + numerics.Add(numeric); + break; + + case "text": + texts ??= new List(); + var text = new MjText(); + text.Parse(child); + texts.Add(text); + break; + + case "tuple": + tuples ??= new List(); + var tuple = new MjTuple(); + tuple.Parse(child); + tuples.Add(tuple); + break; + + default: + Debug.LogWarning($"Unknown custom element: {child.Name}"); + break; + } + } + + public void GenerateCustomMjcf(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("custom"); + foreach (var text in texts) { + var textMjcf = text.ToMjcf(doc); + mjcf.AppendChild(textMjcf); + } + foreach (var numeric in numerics) { + var textMjcf = numeric.ToMjcf(doc); + mjcf.AppendChild(textMjcf); + } + foreach (var tuple in tuples) { + var textMjcf = tuple.ToMjcf(doc); + mjcf.AppendChild(textMjcf); + } + } + + + [Serializable] + private abstract class MjCustomElement { + + [SerializeField] + public string name; + + public void Parse(XmlElement mjcf) { + name = mjcf.GetStringAttribute("name"); + ParseInner(mjcf); + } + + protected abstract void ParseInner(XmlElement mjcf); + + public XmlElement ToMjcf(XmlDocument doc) { + var mjcf = ToMjcfInner(doc); + if (!string.IsNullOrEmpty(name)) { + mjcf.SetAttribute("name", name); + } + + return mjcf; + } + + protected abstract XmlElement ToMjcfInner(XmlDocument doc); + + + } + + [Serializable] + private class MjText : MjCustomElement { + + [SerializeField] + public string data; + + protected override void ParseInner(XmlElement mjcf) { + data = mjcf.GetStringAttribute("data"); + } + + protected override XmlElement ToMjcfInner(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("numeric"); + + mjcf.SetAttribute("data", data); + + return mjcf; + } + } + + + [Serializable] + private class MjNumeric : MjCustomElement { + + [SerializeField] + public int size; + + [SerializeField] + public float[] data; + + protected override void ParseInner(XmlElement mjcf) { + if (int.TryParse(mjcf.GetStringAttribute("size", "-1"), out var size)) { + this.size = size; + } + var data = mjcf.GetFloatArrayAttribute("data", new float[] { }); + + if (this.size > -1 && data.Length != this.size) { + Array.Resize(ref data, this.size); + } + this.data = data; + this.size = this.data.Length; + } + + protected override XmlElement ToMjcfInner(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("numeric"); + if (size >= 0) { + mjcf.SetAttribute("size", MjEngineTool.MakeLocaleInvariant($"{size}")); + } + + mjcf.SetAttribute("data", MjEngineTool.ArrayToMjcf(data)); + + return mjcf; + } + } + + [Serializable] + private class MjTuple : MjCustomElement { + + [SerializeField] + protected List tuples = new List(); + + protected override void ParseInner(XmlElement mjcf) { + foreach (XmlElement child in mjcf.ChildNodes) + if (child.Name == "element") { + var tupleElement = new TupleElement(); + tupleElement.Parse(child); + tuples.Add(tupleElement); + } else { + Debug.LogWarning($"Unknown child element in MjTuple: {child.Name}"); + } + } + + protected override XmlElement ToMjcfInner(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("numeric"); + foreach (var tuple in tuples) { + var tupleMjcf = tuple.ToMjcf(doc); + mjcf.AppendChild(tupleMjcf); + } + + return mjcf; + } + + [Serializable] + protected class TupleElement { + + [SerializeField] + public string objType; + + [SerializeField] + public string objName; + + [SerializeField] + public float prm; + + public void Parse(XmlElement element) { + objType = element.GetAttribute("objtype"); + objName = element.GetAttribute("objname"); + + element.GetFloatAttribute("prm", float.NaN); + } + + public XmlElement ToMjcf(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("element"); + + mjcf.SetAttribute("objType", objType); + mjcf.SetAttribute("objName", objName); + if (!float.IsNaN(prm)) { + mjcf.SetAttribute("prm", $"{prm}"); + } + + return mjcf; + } + + } + } +} +} \ No newline at end of file diff --git a/unity/Runtime/Components/Custom/MjCustom.cs.meta b/unity/Runtime/Components/Custom/MjCustom.cs.meta new file mode 100644 index 0000000000..dc17390962 --- /dev/null +++ b/unity/Runtime/Components/Custom/MjCustom.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6ce590688f88ca4180dc0073783af07 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Deformable.meta b/unity/Runtime/Components/Deformable.meta new file mode 100644 index 0000000000..aaa0f809ac --- /dev/null +++ b/unity/Runtime/Components/Deformable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6fb0a0a18d41c834cb0a6acfc2580bba +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Deformable/MjFlexDeformable.cs b/unity/Runtime/Components/Deformable/MjFlexDeformable.cs new file mode 100644 index 0000000000..ea00fb109b --- /dev/null +++ b/unity/Runtime/Components/Deformable/MjFlexDeformable.cs @@ -0,0 +1,241 @@ +// 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.Linq; +using System.Xml; +using UnityEditor; +using UnityEngine; + +namespace Mujoco { + +public class MjFlexDeformable : MjComponent { + + public string FlexName; + + public int Dim = 2; + + public float Radius = 0.005f; + + public MjBaseBody[] Body; + + public float[] Vertex; + + public float[] Texcoord; + + public int[] Element; + + public bool Flatskin; + + public int Group; + + // Edge, elasticity and contact get special treatment. As they are separate elements in + // MJCF, and are unique to one instance each, they get toggles whether they are included + // or not. If the toggle is false, they are not rendered via MjFlexDeformableEditor, + // and are not added to the generated MJCF. This lets MuJoCo "do its thing" implicitly + // when parsing the scene, which is more robust if, e.g., the defaults change (where + // forced explicit definitions silently alter the scene). + + // Due to their low complexity, no further hierarchy and uniqueness I opted not to have + // separate Unity MonoBehaviours and GameObjects in Unity for the child elements of flex. + // H + public bool ConfigureEdge; + + [SerializeField] + private MjFlexEdge Edge; + + public bool ConfigureContact; + + [SerializeField] + public MjFlexContact Contact; + + public bool ConfigureElasticity; + + [SerializeField] + public MjFlexElasticity Elasticity; + + protected override bool _suppressNameAttribute => true; + + public override MujocoLib.mjtObj ObjectType => MujocoLib.mjtObj.mjOBJ_PLUGIN; + + // Parse the component settings from an external Mjcf. + protected override void OnParseMjcf(XmlElement mjcf) { + FlexName = mjcf.GetStringAttribute("name", ""); + Dim = mjcf.GetIntAttribute("dim", 2); + Radius = mjcf.GetFloatAttribute("radius", 0.005f); + Body = mjcf.GetStringAttribute("body").Split(" ", StringSplitOptions.RemoveEmptyEntries) + .Select(MjHierarchyTool.FindComponentOfTypeAndName).ToArray(); + Vertex = mjcf.GetFloatArrayAttribute("vertex", Array.Empty()); + Texcoord = mjcf.GetFloatArrayAttribute("texcoord", Array.Empty()); + Element = mjcf.GetIntArrayAttribute("element", Array.Empty()); + Flatskin = mjcf.GetBoolAttribute("flatskin"); + Group = mjcf.GetIntAttribute("group"); + + // Parse child elements + var edgeElement = mjcf.SelectSingleNode("edge") as XmlElement; + if (edgeElement != null) { + ConfigureEdge = true; + Edge = new MjFlexEdge(); + Edge.FromMjcf(edgeElement); + } else { + Edge = null; + } + + var elasticityElement = mjcf.SelectSingleNode("elasticity") as XmlElement; + if (elasticityElement != null) { + ConfigureElasticity = true; + Elasticity = new MjFlexElasticity(); + Elasticity.FromMjcf(elasticityElement); + } else { + Elasticity = null; + } + + var contactElement = mjcf.SelectSingleNode("contact") as XmlElement; + if (contactElement != null) { + ConfigureContact = true; + Contact = new MjFlexContact(); + Contact.FromMjcf(contactElement); + } else { + Contact = null; + } + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("flex"); + mjcf.SetAttribute("name", MjEngineTool.MakeLocaleInvariant($"{FlexName}")); + mjcf.SetAttribute("dim", MjEngineTool.MakeLocaleInvariant($"{Dim}")); + mjcf.SetAttribute("radius", MjEngineTool.MakeLocaleInvariant($"{Radius}")); + if (Body.Length > 0) + mjcf.SetAttribute("body", MjEngineTool.ArrayToMjcf(Body.Select(b => b.MujocoName).ToArray())); + if (Vertex.Length > 0) + mjcf.SetAttribute("vertex", MjEngineTool.ArrayToMjcf(Vertex)); + if (Texcoord.Length > 0) + mjcf.SetAttribute("texcoord", MjEngineTool.ArrayToMjcf(Texcoord)); + if (Element.Length > 0) + mjcf.SetAttribute("element", MjEngineTool.ArrayToMjcf(Element)); + mjcf.SetAttribute("flatskin", + MjEngineTool.MakeLocaleInvariant($"{(Flatskin ? "true" : "false")}")); + mjcf.SetAttribute("group", MjEngineTool.MakeLocaleInvariant($"{Group}")); + + // Add child elements if configured + if (ConfigureEdge && Edge != null) { + var edgeElement = doc.CreateElement("edge"); + Edge.ToMjcf(edgeElement); + mjcf.AppendChild(edgeElement); + } + + if (ConfigureElasticity && Elasticity != null) { + var elasticityElement = doc.CreateElement("elasticity"); + Elasticity.ToMjcf(elasticityElement); + mjcf.AppendChild(elasticityElement); + } + + if (ConfigureContact && Contact != null) { + var contactElement = doc.CreateElement("contact"); + Contact.ToMjcf(contactElement); + mjcf.AppendChild(contactElement); + } + + return mjcf; + } + + [Serializable] + public class MjFlexEdge { + [SerializeField] + public float Stiffness = 0; + + [SerializeField] + public float Damping = 0; + + public void ToMjcf(XmlElement mjcf) { + mjcf.SetAttribute("stiffness", MjEngineTool.MakeLocaleInvariant($"{Stiffness}")); + mjcf.SetAttribute("damping", MjEngineTool.MakeLocaleInvariant($"{Damping}")); + } + + // We check the attribute and only overwrite the field instead of giving a default to + // the attribute getter. This way the default only lives in one place (the initializer) + // so it may only need to be changed there. + public void FromMjcf(XmlElement mjcf) { + if (mjcf.HasAttribute("stiffness")) + Stiffness = mjcf.GetFloatAttribute("stiffness"); + if (mjcf.HasAttribute("damping")) + Damping = mjcf.GetFloatAttribute("damping"); + } + } + + [Serializable] + public class MjFlexElasticity { + [SerializeField] + public float Young = 0; + + [SerializeField] + public float Poisson = 0; + + [SerializeField] + public float Damping = 0; + + [SerializeField] + public float Thickness = -1; + + public void ToMjcf(XmlElement mjcf) { + mjcf.SetAttribute("young", MjEngineTool.MakeLocaleInvariant($"{Young}")); + mjcf.SetAttribute("poisson", MjEngineTool.MakeLocaleInvariant($"{Poisson}")); + mjcf.SetAttribute("damping", MjEngineTool.MakeLocaleInvariant($"{Damping}")); + mjcf.SetAttribute("thickness", MjEngineTool.MakeLocaleInvariant($"{Thickness}")); + } + + public void FromMjcf(XmlElement mjcf) { + if (mjcf.HasAttribute("young")) + Young = mjcf.GetFloatAttribute("young"); + if (mjcf.HasAttribute("poisson")) + Poisson = mjcf.GetFloatAttribute("poisson"); + if (mjcf.HasAttribute("damping")) + Damping = mjcf.GetFloatAttribute("damping"); + if (mjcf.HasAttribute("thickness")) + Thickness = mjcf.GetFloatAttribute("thickness"); + } + } + + [Serializable] + public class MjFlexContact { + [SerializeField] + public bool Internal = true; + + [SerializeField] + public string Selfcollide = "auto"; // Would an enum be better? + + [SerializeField] + public int Activelayers = 1; + + public void ToMjcf(XmlElement mjcf) { + mjcf.SetAttribute("internal", + MjEngineTool.MakeLocaleInvariant($"{(Internal ? "true" : "false")}")); + mjcf.SetAttribute("selfcollide", Selfcollide); + mjcf.SetAttribute("activelayers", MjEngineTool.MakeLocaleInvariant($"{Activelayers}")); + } + + public void FromMjcf(XmlElement mjcf) { + if (mjcf.HasAttribute("internal")) + Internal = mjcf.GetBoolAttribute("internal"); + if (mjcf.HasAttribute("selfcollide")) + Selfcollide = mjcf.GetStringAttribute("selfcollide"); + if (mjcf.HasAttribute("activelayers")) + Activelayers = mjcf.GetIntAttribute("activelayers"); + } + } + + +} +} \ No newline at end of file diff --git a/unity/Runtime/Components/Deformable/MjFlexDeformable.cs.meta b/unity/Runtime/Components/Deformable/MjFlexDeformable.cs.meta new file mode 100644 index 0000000000..d9a675b27e --- /dev/null +++ b/unity/Runtime/Components/Deformable/MjFlexDeformable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91d472e67d916114a9b84adbff38ca3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs b/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs new file mode 100644 index 0000000000..29d048b494 --- /dev/null +++ b/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.RegularExpressions; +using Unity.Collections; +using UnityEngine; + +namespace Mujoco { +/// +/// This is a very early stage mesh generator for flex objects. +/// Texture mapping in particular is very rudimentary. +/// Use the context menu button to generate mesh from the inspector. +/// +[RequireComponent(typeof(SkinnedMeshRenderer))] +[ExecuteInEditMode] +public class MjFlexMeshBuilder : MonoBehaviour { + + private SkinnedMeshRenderer _meshRenderer; + + [SerializeField] + MjFlexDeformable flex; + + [SerializeField] + Vector3 uvProjectionU; + + [SerializeField] + Vector3 uvProjectionV; + + [SerializeField] + bool doubleSided; + +protected void Awake() { + _meshRenderer = GetComponent(); + } + + [ContextMenu("Generate Mesh")] + public void GenerateSkinnedMesh() { + DisposeCurrentMesh(); + List vertices = flex.Body.Select(b => b.transform.localPosition).ToList(); + if (doubleSided) vertices.AddRange(vertices.ToArray().Reverse()); + List transforms = flex.Body.Select(b => b.transform).ToList(); + if (doubleSided) transforms.AddRange(transforms.ToArray().Reverse()); + var mesh = new Mesh(); + mesh.name = $"Mujoco mesh for composite {name}"; + mesh.vertices = vertices.ToArray(); + var triangles = flex.Element.ToList(); + if (doubleSided) triangles.AddRange(triangles.ToArray().Reverse().Select(t => vertices.Count/2+t)); + mesh.triangles = triangles.ToArray(); + + + // Generate UVs based on vertex positions + Vector2[] uv = new Vector2[vertices.Count]; + + + // Flexcoord could be used for UV + if (flex.Texcoord != null && flex.Texcoord.Length == vertices.Count * 2) { + Vector2[] texcoords = new Vector2[vertices.Count]; + for (int i = 0; i < vertices.Count; i++) { + texcoords[i] = new Vector2(flex.Texcoord[i * 2], flex.Texcoord[i * 2 + 1]); + } + mesh.uv = texcoords; + } else { + for (int i = 0; i < vertices.Count; i++) { + // Assuming horizontal layout + uv[i] = new Vector2( + Mathf.InverseLerp(vertices.Min(u => Vector3.Dot(u, uvProjectionU)), vertices.Max(u => Vector3.Dot(u, uvProjectionU)), Vector3.Dot(vertices[i], uvProjectionU) ), + Mathf.InverseLerp(vertices.Min(u => Vector3.Dot(u, uvProjectionV)), vertices.Max(u => Vector3.Dot(u, uvProjectionV)), Vector3.Dot(vertices[i], uvProjectionV)) + ); + } + mesh.uv = uv; + } + + // Calculate tangents + mesh.RecalculateNormals(); + Vector4[] tangents = new Vector4[vertices.Count]; + Vector3[] normals = mesh.normals; + + for (int i = 0; i < vertices.Count; i++) { + Vector3 normal = normals[i]; + Vector3 tangent = Vector3.Cross(normal, Vector3.up).normalized; + if (tangent.magnitude < 0.01f) { + tangent = Vector3.Cross(normal, Vector3.right).normalized; + } + tangents[i] = new Vector4(tangent.x, tangent.y, tangent.z, -1f); + } + mesh.tangents = tangents; + + // Bone weights setup + byte[] bonesPerVertex = Enumerable.Repeat(1, vertices.Count) + .Select(i => (byte)i).ToArray(); + BoneWeight1[] boneWeights = transforms + .Select((t, i) => new BoneWeight1 { boneIndex = i, weight = 1 }).ToArray(); + mesh.SetBoneWeights(new NativeArray(bonesPerVertex, Allocator.Temp), + new NativeArray(boneWeights, Allocator.Temp)); + mesh.bindposes = transforms + .Select(t => Matrix4x4.TRS(t.localPosition, t.localRotation, Vector3.one).inverse) + .ToArray(); + _meshRenderer.sharedMesh = mesh; + _meshRenderer.bones = transforms.ToArray(); + } + + protected void OnDestroy() { + DisposeCurrentMesh(); + } + + // Dynamically created meshes with no references are only disposed automatically on scene changes. + // This prevents resource leaks in case the host environment doesn't reload scenes. + private void DisposeCurrentMesh() { + if (_meshRenderer.sharedMesh != null) { +#if UNITY_EDITOR + DestroyImmediate(_meshRenderer.sharedMesh); +#else + Destroy(_meshFilter.sharedMesh); +#endif + } + } + + + +} +} \ No newline at end of file diff --git a/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs.meta b/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs.meta new file mode 100644 index 0000000000..696b59c1ef --- /dev/null +++ b/unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0309a15d7c975434596170c74abb23d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Equality/MjConnect.cs b/unity/Runtime/Components/Equality/MjConnect.cs index 69e17e0655..f442e5558f 100644 --- a/unity/Runtime/Components/Equality/MjConnect.cs +++ b/unity/Runtime/Components/Equality/MjConnect.cs @@ -22,12 +22,16 @@ namespace Mujoco { public class MjConnect : MjBaseConstraint { public MjBaseBody Body1; public MjBaseBody Body2; + public MjSite Site1; + public MjSite Site2; public Transform Anchor; protected override string _constraintName => "connect"; protected override void FromMjcf(XmlElement mjcf) { Body1 = mjcf.GetObjectReferenceAttribute("body1"); Body2 = mjcf.GetObjectReferenceAttribute("body2"); + Site1 = mjcf.GetObjectReferenceAttribute("site1"); + Site2 = mjcf.GetObjectReferenceAttribute("site2"); var anchorPos = MjEngineTool.UnityVector3( mjcf.GetVector3Attribute("anchor", defaultValue: Vector3.zero)); Anchor = new GameObject("connect_anchor").transform; @@ -37,16 +41,21 @@ protected override void FromMjcf(XmlElement mjcf) { // Generate implementation specific XML element. protected override void ToMjcf(XmlElement mjcf) { - if (Body1 == null || Body2 == null) { - throw new NullReferenceException($"Both bodies in connect {name} are required."); + if (!(Body1 && Anchor) && !(Site1 && Site2)) { + throw new NullReferenceException($"Either body1 and anchor is required or both sites have to be defined in {name}."); } - if (Anchor == null) { - throw new NullReferenceException($"Anchor in connect {name} is required."); - } - mjcf.SetAttribute("body1", Body1.MujocoName); - mjcf.SetAttribute("body2", Body2.MujocoName); - mjcf.SetAttribute("anchor", - MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(Anchor.localPosition))); + if (Body1) + mjcf.SetAttribute("body1", Body1.MujocoName); + if (Body2) + mjcf.SetAttribute("body2", Body2.MujocoName); + if (Anchor) + mjcf.SetAttribute("anchor", + MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(Anchor.localPosition))); + if (Site1) + mjcf.SetAttribute("site1", Site1.MujocoName); + if (Site2) + mjcf.SetAttribute("site2", Site2.MujocoName); + } public void OnValidate() { diff --git a/unity/Runtime/Components/Equality/MjFlexConstraint.cs b/unity/Runtime/Components/Equality/MjFlexConstraint.cs new file mode 100644 index 0000000000..c60934aa06 --- /dev/null +++ b/unity/Runtime/Components/Equality/MjFlexConstraint.cs @@ -0,0 +1,35 @@ +// 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.Linq; +using System.Xml; +using UnityEngine; + +namespace Mujoco { + + public class MjFlexConstraint : MjBaseConstraint { + public string Flex; + protected override string _constraintName => "flex"; + + protected override void FromMjcf(XmlElement mjcf) { + Flex = mjcf.GetStringAttribute("flex"); + } + + protected override void ToMjcf(XmlElement mjcf) { + mjcf.SetAttribute("flex", Flex); + } + + } +} diff --git a/unity/Runtime/Components/Equality/MjFlexConstraint.cs.meta b/unity/Runtime/Components/Equality/MjFlexConstraint.cs.meta new file mode 100644 index 0000000000..2868da252e --- /dev/null +++ b/unity/Runtime/Components/Equality/MjFlexConstraint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e94174b95e75da4a8e12d4a8d1d9d5d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Equality/MjWeld.cs b/unity/Runtime/Components/Equality/MjWeld.cs index 7c628525ce..69a8243403 100644 --- a/unity/Runtime/Components/Equality/MjWeld.cs +++ b/unity/Runtime/Components/Equality/MjWeld.cs @@ -23,6 +23,8 @@ public class MjWeld : MjBaseConstraint { public MjBaseBody Body1; public MjBaseBody Body2; public Transform WeldOffset; + public float Torquescale; + public float[] Relpose; protected override string _constraintName => "weld"; protected override unsafe void OnBindToRuntime(MujocoLib.mjModel_* model, MujocoLib.mjData_* data) { @@ -36,10 +38,13 @@ protected override unsafe void OnBindToRuntime(MujocoLib.mjModel_* model, Mujoco protected override void FromMjcf(XmlElement mjcf) { Body1 = mjcf.GetObjectReferenceAttribute("body1"); Body2 = mjcf.GetObjectReferenceAttribute("body2"); - var relpose = mjcf.GetStringAttribute("relpose"); - if (relpose != null) { + Torquescale = mjcf.GetFloatAttribute("torquescale"); + Relpose = mjcf.GetFloatArrayAttribute("relpose", new [] {0, 1, 0, 0, 0, 0, 0f}); + + var weldOffset = mjcf.GetStringAttribute("weldoffset"); + if (weldOffset != null) { Debug.Log( - $"relpose {relpose} in weld {name} ignored. Set WeldOffset in the editor."); + $"Weldoffset {weldOffset} in weld {name} ignored. Set WeldOffset in the editor."); } } @@ -50,6 +55,8 @@ protected override void ToMjcf(XmlElement mjcf) { mjcf.SetAttribute("body1", Body1.MujocoName); mjcf.SetAttribute("body2", Body2.MujocoName); + mjcf.SetAttribute("relpose", MjEngineTool.ArrayToMjcf(Relpose)); + mjcf.SetAttribute("torquescale", MjEngineTool.MakeLocaleInvariant($"{Torquescale}")); } public void OnValidate() { diff --git a/unity/Runtime/Components/MjActuator.cs b/unity/Runtime/Components/MjActuator.cs index 1c5ff755e8..f0659b8948 100644 --- a/unity/Runtime/Components/MjActuator.cs +++ b/unity/Runtime/Components/MjActuator.cs @@ -282,13 +282,16 @@ public void MuscleFromMjcf(XmlElement mjcf) { public ActuatorType Type; - [Tooltip("Joint actuation target. Mutually exclusive with tendon target.")] + [Tooltip("Joint actuation target. Mutually exclusive with tendon and site target.")] public MjBaseJoint Joint; - [Tooltip("Tendon actuation target. Mutually exclusive with joint target.")] + [Tooltip("Tendon actuation target. Mutually exclusive with joint and site target.")] public MjBaseTendon Tendon; - [Tooltip("Parameters specific to each actuator type.")] + [Tooltip("Tendon actuation target. Mutually exclusive with joint and tendon target.")] + public MjSite Site; + + [Tooltip("Parameters specific to each actuator type.")] [HideInInspector] public CustomParameters CustomParams = new CustomParameters(); @@ -339,23 +342,26 @@ protected override void OnParseMjcf(XmlElement mjcf) { } Joint = mjcf.GetObjectReferenceAttribute("joint"); Tendon = mjcf.GetObjectReferenceAttribute("tendon"); + Site = mjcf.GetObjectReferenceAttribute("site"); } // Generate implementation specific XML element. protected override XmlElement OnGenerateMjcf(XmlDocument doc) { - if (Joint == null && Tendon == null) { - throw new InvalidOperationException($"Actuator {name} is not assigned a joint nor tendon."); + if (Joint == null && Tendon == null && Site == null) { + throw new InvalidOperationException($"Actuator {name} is not assigned a joint, tendon or site."); } - if (Joint != null && Tendon != null) { + if (new[]{Joint != null, Tendon != null, Site != null}.Count(x => x) > 1) { throw new InvalidOperationException( - $"Actuator {name} can't have both a tendon and a joint target."); + $"Actuator {name} can't have more than one target, joint, tendon and site are mutually exclusive."); } var mjcf = doc.CreateElement(Type.ToString().ToLowerInvariant()); if (Joint != null) { mjcf.SetAttribute("joint", Joint.MujocoName); - } else { + } else if(Tendon != null) { mjcf.SetAttribute("tendon", Tendon.MujocoName); + } else { + mjcf.SetAttribute("site", Site.MujocoName); } CommonParams.ToMjcf(mjcf); diff --git a/unity/Runtime/Components/MjGlobalSettings.cs b/unity/Runtime/Components/MjGlobalSettings.cs index d41cefe571..176ed29cce 100644 --- a/unity/Runtime/Components/MjGlobalSettings.cs +++ b/unity/Runtime/Components/MjGlobalSettings.cs @@ -253,12 +253,12 @@ public void ParseMjcf(XmlElement mjcf) { Jacobian = mjcf.GetEnumAttribute("jacobian", localDefault.Jacobian); Solver = mjcf.GetEnumAttribute("solver", localDefault.Solver); - Iterations = (int)mjcf.GetFloatAttribute("iterations", localDefault.Iterations); + Iterations = mjcf.GetIntAttribute("iterations", localDefault.Iterations); Tolerance = mjcf.GetFloatAttribute("tolerance", localDefault.Tolerance); - NoSlipIterations = (int)mjcf.GetFloatAttribute( + NoSlipIterations = mjcf.GetIntAttribute( "noslip_iterations", localDefault.NoSlipIterations); NoSlipTolerance = mjcf.GetFloatAttribute("noslip_tolerance", localDefault.NoSlipTolerance); - CcdIterations = (int)mjcf.GetFloatAttribute("ccd_iterations", localDefault.CcdIterations); + CcdIterations = mjcf.GetIntAttribute("ccd_iterations", localDefault.CcdIterations); CcdTolerance = mjcf.GetFloatAttribute("ccd_tolerance", localDefault.CcdTolerance); var flagElements = mjcf.GetElementsByTagName("flag"); diff --git a/unity/Runtime/Components/MjScene.cs b/unity/Runtime/Components/MjScene.cs index c55c5cf52c..72a81e0398 100644 --- a/unity/Runtime/Components/MjScene.cs +++ b/unity/Runtime/Components/MjScene.cs @@ -393,9 +393,24 @@ private XmlDocument GenerateSceneMjcf(IEnumerable components) { (component is MjInertial) || (component is MjBaseJoint) || (component is MjGeom) || - (component is MjSite)), + (component is MjSite)|| + (component is MjPluginTag)), worldMjcf); + //MuJoCo plug-ins have some hierarchical structure too. + var extensionObjects = components.Where(component => + (component is MjPlugin) || + (component is MjPluginInstance) || + (component is MjPluginConfig)).ToList(); + if (extensionObjects.Count > 0) { + var extensionMjcf = (XmlElement)MjRoot.AppendChild(doc.CreateElement("extension")); + BuildHierarchicalMjcf(doc, extensionObjects, extensionMjcf); + } + + if (MjCustom.InstanceExists) { + MjCustom.Instance.GenerateCustomMjcf(doc); + } + // Non-hierarchical sections: MjRoot.AppendChild(GenerateMjcfSection( doc, components.Where(component => component is MjExclude), "contact")); @@ -403,7 +418,11 @@ private XmlDocument GenerateSceneMjcf(IEnumerable components) { MjRoot.AppendChild(GenerateMjcfSection( doc, components.Where(component => component is MjBaseTendon), "tendon")); + //MuJoCo skins would also be applicable, but we skip them since they are purely visual. MjRoot.AppendChild(GenerateMjcfSection( + doc, components.Where(component => component is MjFlexDeformable), "deformable")); + + MjRoot.AppendChild(GenerateMjcfSection( doc, components.Where(component => component is MjBaseConstraint), "equality")); MjRoot.AppendChild( diff --git a/unity/Runtime/Components/Plugins.meta b/unity/Runtime/Components/Plugins.meta new file mode 100644 index 0000000000..60755ef566 --- /dev/null +++ b/unity/Runtime/Components/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cc69a4999eb8125449538a43ba3e8726 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Plugins/MjPlugin.cs b/unity/Runtime/Components/Plugins/MjPlugin.cs new file mode 100644 index 0000000000..96618625c1 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPlugin.cs @@ -0,0 +1,52 @@ +// 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 { + +public class MjPlugin : MjComponent { + + //Plugin identifier, used for implicit plugin instantiation. + public string Plugin = ""; + + //Instance name, used for explicit plugin instantiation. + public string Instance = ""; + + protected override bool _suppressNameAttribute => true; + + public override MujocoLib.mjtObj ObjectType => MujocoLib.mjtObj.mjOBJ_PLUGIN; + + // Parse the component settings from an external Mjcf. + protected override void OnParseMjcf(XmlElement mjcf) { + Plugin = mjcf.GetStringAttribute("plugin", ""); + Instance = mjcf.GetStringAttribute("instance", ""); + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + + var mjcf = (XmlElement)doc.CreateElement("plugin"); + if (Plugin.Length > 0) + mjcf.SetAttribute("plugin", Plugin); + if (Instance.Length > 0) + mjcf.SetAttribute("instance", Instance); + return mjcf; + } +} +} diff --git a/unity/Runtime/Components/Plugins/MjPlugin.cs.meta b/unity/Runtime/Components/Plugins/MjPlugin.cs.meta new file mode 100644 index 0000000000..df9b7eee73 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPlugin.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 888af6d4500255440bb9e642774e5562 +timeCreated: 1546881319 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Plugins/MjPluginConfig.cs b/unity/Runtime/Components/Plugins/MjPluginConfig.cs new file mode 100644 index 0000000000..159c056214 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginConfig.cs @@ -0,0 +1,47 @@ +// 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 { +public class MjPluginConfig : MjComponent { + public string Key = ""; + public string Value = ""; + + protected override bool _suppressNameAttribute => true; + + public override MujocoLib.mjtObj ObjectType => MujocoLib.mjtObj.mjOBJ_PLUGIN; + + // Parse the component settings from an external Mjcf. + protected override void OnParseMjcf(XmlElement mjcf) { + Key = mjcf.GetStringAttribute("key", ""); + Value = mjcf.GetStringAttribute("value", ""); + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + + var mjcf = (XmlElement)doc.CreateElement("config"); + if (Key.Length > 0) + mjcf.SetAttribute("key", Key); + if (Value.Length > 0) + mjcf.SetAttribute("value", Value); + return mjcf; + } +} +} diff --git a/unity/Runtime/Components/Plugins/MjPluginConfig.cs.meta b/unity/Runtime/Components/Plugins/MjPluginConfig.cs.meta new file mode 100644 index 0000000000..b203dbc2ba --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginConfig.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: f4ae5a1748311254d9a620a56e3b50b9 +timeCreated: 1546881319 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Plugins/MjPluginInstance.cs b/unity/Runtime/Components/Plugins/MjPluginInstance.cs new file mode 100644 index 0000000000..a670fbdade --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginInstance.cs @@ -0,0 +1,45 @@ +// 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 { +// Actuators provide means to set joints in motion. +public class MjPluginInstance : MjComponent { + //Instance name, used for explicit plugin instantiation. + public string Name = ""; + + protected override bool _suppressNameAttribute => true; + + public override MujocoLib.mjtObj ObjectType => MujocoLib.mjtObj.mjOBJ_UNKNOWN; + + // Parse the component settings from an external Mjcf. + protected override void OnParseMjcf(XmlElement mjcf) { + Name = mjcf.GetStringAttribute("name", ""); + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + if (string.IsNullOrEmpty(Name)) + throw new ArgumentException($"Attribute \"name\" is required for {nameof(MjPluginInstance)}."); + var mjcf = (XmlElement)doc.CreateElement("instance"); + mjcf.SetAttribute("name", Name); + return mjcf; + } +} +} diff --git a/unity/Runtime/Components/Plugins/MjPluginInstance.cs.meta b/unity/Runtime/Components/Plugins/MjPluginInstance.cs.meta new file mode 100644 index 0000000000..e410842dba --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginInstance.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: f90bbc7e217d39141865e8be937d818a +timeCreated: 1546881319 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Plugins/MjPluginTag.cs b/unity/Runtime/Components/Plugins/MjPluginTag.cs new file mode 100644 index 0000000000..552fa2ce71 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginTag.cs @@ -0,0 +1,52 @@ +// 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 { + +public class MjPluginTag : MjComponent { + + //Plugin identifier, used for implicit plugin instantiation. + public string Plugin = ""; + + //Instance name, used for explicit plugin instantiation. + public string Instance = ""; + + protected override bool _suppressNameAttribute => true; + + public override MujocoLib.mjtObj ObjectType => MujocoLib.mjtObj.mjOBJ_PLUGIN; + + // Parse the component settings from an external Mjcf. + protected override void OnParseMjcf(XmlElement mjcf) { + Plugin = mjcf.GetStringAttribute("plugin", ""); + Instance = mjcf.GetStringAttribute("instance", ""); + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + + var mjcf = (XmlElement)doc.CreateElement("plugin"); + if (Plugin.Length > 0) + mjcf.SetAttribute("plugin", Plugin); + if (Instance.Length > 0) + mjcf.SetAttribute("instance", Instance); + return mjcf; + } +} +} diff --git a/unity/Runtime/Components/Plugins/MjPluginTag.cs.meta b/unity/Runtime/Components/Plugins/MjPluginTag.cs.meta new file mode 100644 index 0000000000..09898e8621 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginTag.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 0c475b032405be74b84c033b55da832e +timeCreated: 1546881319 +licenseType: Pro +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Shapes/MjGeomSettings.cs b/unity/Runtime/Components/Shapes/MjGeomSettings.cs index 5c887f1bda..081ca1f846 100644 --- a/unity/Runtime/Components/Shapes/MjGeomSettings.cs +++ b/unity/Runtime/Components/Shapes/MjGeomSettings.cs @@ -57,16 +57,16 @@ public enum FluidShapeTypes { public void FromMjcf(XmlElement mjcf) { - Priority = (int) mjcf.GetFloatAttribute("priority", 0); + Priority = mjcf.GetIntAttribute("priority", 0); // Contact filtering settings. - Filtering.Contype = (int)mjcf.GetFloatAttribute("contype", CollisionFiltering.Default.Contype); - Filtering.Conaffinity = (int)mjcf.GetFloatAttribute( + Filtering.Contype = mjcf.GetIntAttribute("contype", CollisionFiltering.Default.Contype); + Filtering.Conaffinity = mjcf.GetIntAttribute( "conaffinity", CollisionFiltering.Default.Conaffinity); - Filtering.Group = (int)mjcf.GetFloatAttribute("group", CollisionFiltering.Default.Group); + Filtering.Group = mjcf.GetIntAttribute("group", CollisionFiltering.Default.Group); // Solver settings. - Solver.ConDim = (int)mjcf.GetFloatAttribute("condim", GeomSolver.Default.ConDim); + Solver.ConDim = mjcf.GetIntAttribute("condim", GeomSolver.Default.ConDim); Solver.SolMix = mjcf.GetFloatAttribute("solmix", GeomSolver.Default.SolMix); var solref = mjcf.GetFloatArrayAttribute( "solref", new float[] { GeomSolver.Default.SolRef.TimeConst, diff --git a/unity/Runtime/Importer/MjcfImporter.cs b/unity/Runtime/Importer/MjcfImporter.cs index 7218100705..e0468ad778 100644 --- a/unity/Runtime/Importer/MjcfImporter.cs +++ b/unity/Runtime/Importer/MjcfImporter.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml; +using UnityEditor; using UnityEngine; namespace Mujoco { @@ -142,8 +143,28 @@ protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { settingsComponent.ParseGlobalMjcfSections(mujocoNode); } - // This makes references to assets. - var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement; + // This section introduces parameters for user plugins. + var extensionNode = mujocoNode.SelectSingleNode("extension") as XmlElement; + if (extensionNode != null) { + var extensionParentObject = CreateGameObjectInParent("extension", rootObject); + foreach (var child in extensionNode.OfType()) { + _modifiers.ApplyModifiersToElement(child); + ParseExtensions(extensionParentObject, extensionNode); + } + } + + var customNode = mujocoNode.SelectSingleNode("custom") as XmlElement; + if (customNode != null) { + var customObject = CreateGameObjectInParent("custom", rootObject); + var customComponent = customObject.AddComponent(); + foreach (var child in customNode.OfType()) { + _modifiers.ApplyModifiersToElement(child); + customComponent.ParseCustom(child); + } + } + + // This makes references to assets. + var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement; ParseBodyChildren(rootObject, worldBodyNode); // This section references bodies, must be parsed after worldbody. @@ -178,8 +199,18 @@ protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { } } - // This section references worldbody elements + tendons, must be parsed after them. - var equalityNode = mujocoNode.SelectSingleNode("equality") as XmlElement; + var deformableNode = mujocoNode.SelectSingleNode("deformable") as XmlElement; + if (deformableNode != null) { + var deformableParentObject = CreateGameObjectInParent("deformable configuration", rootObject); + foreach (var child in deformableNode.OfType()) { + var deformableType = ParseDeformableType(child); + _modifiers.ApplyModifiersToElement(child); + CreateGameObjectWithUniqueName(deformableParentObject, child, deformableType); + } + } + + // 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()) { @@ -239,6 +270,42 @@ private void ParseBodyChildren(GameObject parentObject, XmlElement parentNode) { } } + private void ParseExtensions(GameObject parentObject, XmlElement parentNode) { + foreach (var child in parentNode.Cast().OfType()) { + _modifiers.ApplyModifiersToElement(child); + + if (_customNodeHandlers.TryGetValue(child.Name, out var handler)) { + handler?.Invoke(child, parentObject); + } else { + ParseExtension(child, parentObject); + } + } + } + + private void ParseExtension(XmlElement child, GameObject parentObject) { + switch (child.Name) { + + case "plugin": { + var pluginObject = CreateGameObjectWithUniqueName(parentObject, child); + ParseBodyChildren(pluginObject, child); + break; + } + case "instance": { + var instanceObject = CreateGameObjectWithUniqueName(parentObject, child); + ParseBodyChildren(instanceObject, child); + break; + } + case "config": { + CreateGameObjectWithUniqueName(parentObject, child); + break; + } + default: { + Debug.Log($"The importer does not yet support tags <{child.Name}>."); + break; + } + } + } + // Called by ParseBodyChildren for each XML node, overridable by inheriting classes. private void ParseBodyChild(XmlElement child, GameObject parentObject) { switch (child.Name) { @@ -285,10 +352,22 @@ private void ParseBodyChild(XmlElement child, GameObject parentObject) { break; } case "plugin": { - Debug.Log($"Plugin elements are only partially supported."); + var pluginObject = CreateGameObjectWithUniqueName(parentObject, child); + ParseBodyChildren(pluginObject, child); + break; + } + case "instance": { + var instanceObject = CreateGameObjectWithUniqueName(parentObject, child); + ParseBodyChildren(instanceObject, child); + break; + } + case "numeric": { + break; + } + case "config": { + CreateGameObjectWithUniqueName(parentObject, child); break; } - default: { Debug.Log($"The importer does not yet support tags <{child.Name}>."); break; @@ -311,13 +390,29 @@ private static Type ParseEqualityType(XmlElement node) { case "tendon": equalityType = typeof(MjTendonConstraint); break; + case "flex": + equalityType = typeof(MjFlexConstraint); + break; default: - Debug.Log($"The importer does not yet support equality <{node.Name}>."); + Debug.LogWarning($"The importer does not yet support equality <{node.Name}>."); break; } return equalityType; } + private static Type ParseDeformableType(XmlElement node) { + Type deformableType = null; + switch (node.Name) { + case "flex": + deformableType = typeof(MjFlexDeformable); + break; + default: + Debug.LogWarning($"The importer does not yet support deformable <{node.Name}>."); + break; + } + return deformableType; + } + private static Type ParseSensorType(XmlElement node) { Type sensorType = null; switch (node.Name) { diff --git a/unity/Runtime/Plugins.meta b/unity/Runtime/Plugins.meta new file mode 100644 index 0000000000..44882768f5 --- /dev/null +++ b/unity/Runtime/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6c9b17e9bd8977844bf7185d2b61aa69 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Plugins/README.md b/unity/Runtime/Plugins/README.md new file mode 100644 index 0000000000..53b28c2663 --- /dev/null +++ b/unity/Runtime/Plugins/README.md @@ -0,0 +1,13 @@ +## User MuJoCo Plug-ins + +As Unity's physics functionality is extended by the MuJoCo library, so too can the base MuJoCo engine be extended with user plug-ins. For more information, see the [documentation ](https://mujoco.readthedocs.io/en/latest/programming/extension.html#explugin). Place plug-in libraries in this folder to load them when importing or running a model with user plugins. + +The multiple types of plug-ins in this contexts necessitates some clarification of the terminology: +- MuJoCo engine library: The `.dll` or `.so` file containing the core physics functionality of MuJoCo. +- Unity plug-in: The interface and bindings to the MuJoCo engine library in Unity. +- MuJoCo Unity package: Additional scripts and components provided with the Unity plug-in to facilitate easier scene creation and running. E.g., Unity Editor components to visualise and configure joints, tools to import and export scenes. +- MuJoCo (user) plug-ins: The libraries extending MuJoCo's features even outside of Unity. E.g., the elasticity plugin that adds shorthands for bendable and deformable structures. + +As with the base MuJoCo engine library, the MuJoCo plug-ins must match the version of the Unity plug-in. If you update the version of your Unity plug-in, be sure to update the binaries of the engine and MuJoCo plug-ins too. + +User plugin support for the Unity package is currently experimental, consider comparing results with simulations outside of Unity. \ No newline at end of file diff --git a/unity/Runtime/Plugins/README.md.meta b/unity/Runtime/Plugins/README.md.meta new file mode 100644 index 0000000000..fdec7f68bb --- /dev/null +++ b/unity/Runtime/Plugins/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8b037fe8dea735342a4933bea9b79a29 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Tools/MjEngineTool.cs b/unity/Runtime/Tools/MjEngineTool.cs index bf27a1d950..cc1e7f1242 100644 --- a/unity/Runtime/Tools/MjEngineTool.cs +++ b/unity/Runtime/Tools/MjEngineTool.cs @@ -16,6 +16,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Xml; using UnityEngine; @@ -244,13 +245,32 @@ public static string ArrayToMjcf(float[] array) { return ret.Substring(startIndex:0, length:ret.Length - 1); } + // Note: we could make these (and other parts of the C# code base) + // use generics instead + public static string ArrayToMjcf(int[] array) { + String ret = ""; + foreach (int entry in array) { + ret += MakeLocaleInvariant($"{entry} "); + } + return ret.Substring(startIndex: 0, length: ret.Length - 1); + } + + public static string ArrayToMjcf(string[] array) { + String ret = ""; + foreach (string entry in array) { + ret += MakeLocaleInvariant($"{entry} "); + } + return ret.Substring(startIndex: 0, length: ret.Length - 1); + } + // Converts a list of floats to an Mjcf. public static string ListToMjcf(List list) { String ret = ""; foreach (float entry in list) { ret += MakeLocaleInvariant($"{entry} "); } - return ret.Substring(startIndex:0, length:ret.Length - 1); + + return ret.Substring(startIndex: 0, length: ret.Length - 1); } // Generates an Mjcf of the specified component's transform. @@ -372,12 +392,11 @@ public static void ParseTransformMjcf(XmlElement mjcf, Transform transform) { public static void LoadPlugins() { if(!Directory.Exists("Packages/org.mujoco/Runtime/Plugins/")) return; - foreach (string pluginPath in Directory.GetFiles("Packages/org.mujoco/Runtime/Plugins/")) { - MujocoLib.mj_loadPluginLibrary(pluginPath); + foreach (string pluginPath in Directory.GetFiles("Packages/org.mujoco/Runtime/Plugins/").Where(f => f.EndsWith(".dll") || f.EndsWith(".so"))) { + Debug.Log($"Loading plugin {pluginPath}"); + MujocoLib.mj_loadPluginLibrary(Path.GetFullPath(pluginPath)); } - } - } public static class MjSceneImportSettings { diff --git a/unity/Runtime/Tools/XmlElementExtensions.cs b/unity/Runtime/Tools/XmlElementExtensions.cs index 9dd8996a28..b4e3442eec 100644 --- a/unity/Runtime/Tools/XmlElementExtensions.cs +++ b/unity/Runtime/Tools/XmlElementExtensions.cs @@ -73,8 +73,22 @@ public static float GetFloatAttribute( } } - // The MuJoCo parser is case-sensitive. - public static T GetEnumAttribute( + public static int GetIntAttribute( + this XmlElement element, string name, int defaultValue = 0) { + if (!element.HasAttribute(name)) { + return defaultValue; + } + var strValue = element.GetAttribute(name); + int parsedValue; + if (int.TryParse(strValue, NumberStyles.Any, CultureInfo.InvariantCulture, out parsedValue)) { + return parsedValue; + } else { + throw new ArgumentException($"'{strValue}' is not a int."); + } + } + + // The MuJoCo parser is case-sensitive. + public static T GetEnumAttribute( this XmlElement element, string name, T defaultValue, bool ignoreCase = false) where T : struct, IConvertible { if (!typeof(T).IsEnum) { @@ -186,5 +200,44 @@ public static float[] GetFloatArrayAttribute( } return result; } + + // Parses an array of whitespace separated floating points. + // + // Args: + // . element: XmlElement that contains the attribute to be parsed. + // . name: Name of the attribute to be parsed. + // . defaultValue: An array of floats, or null. A default value to be returned in case the + // the attribute is missing. + // . fillMissingValues: If a default value was provided, and it has more components than the value + // parsed from the attribute, the missing components will be copied from the defaultValue. + public static int[] GetIntArrayAttribute( + this XmlElement element, string name, int[] defaultValue, bool fillMissingValues = true) { + if (!element.HasAttribute(name)) { + return defaultValue; + } + var strValue = element.GetAttribute(name); + var components = strValue.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries); + var resultLength = components.Length; + if (fillMissingValues && defaultValue != null) { + // If filling of the missing values was enabled, and a default value was provided, + // allocate an array large enough to store a value of this length, in case when the parsed + // value has fewer components. + resultLength = Math.Max(resultLength, defaultValue.Length); + } + var result = new int[resultLength]; + for (var i = 0; i < components.Length; ++i) { + int componentValue; + if (int.TryParse(components[i], NumberStyles.Any, CultureInfo.InvariantCulture, out componentValue)) { + result[i] = componentValue; + } else { + throw new ArgumentException($"'{components[i]}' is not a float."); + } + } + for (var i = components.Length; i < resultLength; ++i) { + // Fill the missing values with defaults. + result[i] = defaultValue[i]; + } + return result; + } } }