diff --git a/unity/Runtime/Components/MjGlobalSettings.cs b/unity/Runtime/Components/MjGlobalSettings.cs index 398a707f..e6dccc80 100644 --- a/unity/Runtime/Components/MjGlobalSettings.cs +++ b/unity/Runtime/Components/MjGlobalSettings.cs @@ -143,32 +143,18 @@ public struct MjcfOptionFlag { [Serializable] public struct MjSizeStruct { - public int Njmax; - public int Nconmax; - public int Nstack; + public String Memory; public static MjSizeStruct Default = new MjSizeStruct() { - Njmax = 0, - Nconmax = 0, - Nstack = 0 + Memory = "-1" }; public void ParseMjcf(XmlElement mjcf) { var localDefault = MjSizeStruct.Default; - Njmax = (int)mjcf.GetFloatAttribute("njmax", localDefault.Njmax); - Nconmax = (int)mjcf.GetFloatAttribute("nconmax", localDefault.Nconmax); - Nstack = (int)mjcf.GetFloatAttribute("nstack", localDefault.Nstack); + Memory = mjcf.GetAttribute("memory", localDefault.Memory); } public XmlElement ToMjcf(XmlElement mjcf) { - if (Njmax > 0) { - mjcf.SetAttribute("njmax", $"{Njmax}"); - } - if (Nconmax > 0) { - mjcf.SetAttribute("nconmax", $"{Nconmax}"); - } - if (Nstack > 0) { - mjcf.SetAttribute("nstack", $"{Nstack}"); - } + mjcf.SetAttribute("memory", $"{Memory}"); return mjcf; } } @@ -327,6 +313,13 @@ public struct MjOptionStruct { } } +[Serializable] +public class NumericEntry { + public String Name; + [Tooltip("Space-separated list of floats.")] + public String Data; +} + public class MjGlobalSettings : MonoBehaviour { [Tooltip("Filename for the generated scene XML.")] @@ -339,6 +332,8 @@ public class MjGlobalSettings : MonoBehaviour { public MjSizeStruct GlobalSizes = MjSizeStruct.Default; + public List CustomNumeric = new List() {}; + public static MjGlobalSettings Instance { get { if (_instance == null) { @@ -365,18 +360,44 @@ public class MjGlobalSettings : MonoBehaviour { } } - public void ParseOptionSizeMjcf(XmlElement option, XmlElement size) { - if (option != null) { - GlobalOptions.ParseMjcf(option); + public void ParseGlobalMjcfSections(XmlElement mujocoNode) { + + var optionNode = mujocoNode.SelectSingleNode("option") as XmlElement; + var sizeNode = mujocoNode.SelectSingleNode("size") as XmlElement; + var customNode = mujocoNode.SelectSingleNode("custom") as XmlElement; + + if (optionNode != null) { + GlobalOptions.ParseMjcf(optionNode); } - if (size != null) { - GlobalSizes.ParseMjcf(size); + if (sizeNode != null) { + GlobalSizes.ParseMjcf(sizeNode); + } + if (customNode != null) { + foreach (var childNode in customNode.ChildNodes) { + var child = childNode as XmlElement; + if (child.Name == "numeric") { + var numeric = new NumericEntry(); + numeric.Name = child.GetAttribute("name"); + numeric.Data = child.GetAttribute("data"); + CustomNumeric.Add(numeric); + } + } } } - public void OptionSizeToMjcf(XmlElement option, XmlElement size) { - GlobalOptions.ToMjcf(option); - GlobalSizes.ToMjcf(size); + public void GlobalsToMjcf(XmlElement mjcf) { + var doc = mjcf.OwnerDocument; + var optionMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("option")); + GlobalOptions.ToMjcf(optionMjcf); + var sizeMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("size")); + GlobalSizes.ToMjcf(sizeMjcf); + var customMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("custom")); + foreach (var numeric in CustomNumeric) { + var numericMjcf = (XmlElement)customMjcf.AppendChild(doc.CreateElement("numeric")); + numericMjcf.SetAttribute("name", numeric.Name); + // TODO: add validation that data is a space-separated list of floating numbers? + numericMjcf.SetAttribute("data", numeric.Data); + } } } } diff --git a/unity/Runtime/Importer/MjcfImporter.cs b/unity/Runtime/Importer/MjcfImporter.cs index dabb36ef..48c88a92 100644 --- a/unity/Runtime/Importer/MjcfImporter.cs +++ b/unity/Runtime/Importer/MjcfImporter.cs @@ -133,30 +133,25 @@ public class MjcfImporter { } } - protected virtual void ParseRoot(GameObject parentObject, XmlElement parentNode) { + protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { - // This makes no references nor being referred into, so it can be parsed whenever. - var optionNode = parentNode.SelectSingleNode("option") as XmlElement; - var sizeNode = parentNode.SelectSingleNode("size") as XmlElement; - if (optionNode != null || sizeNode != null) { - var globalsObject = CreateGameObjectInParent("Global Settings", parentObject); - var settingsComponent = globalsObject.AddComponent(); - settingsComponent.ParseOptionSizeMjcf(optionNode, sizeNode); - } + var globalsObject = CreateGameObjectInParent("Global Settings", rootObject); + var settingsComponent = globalsObject.AddComponent(); + settingsComponent.ParseGlobalMjcfSections(mujocoNode); // This makes references to assets. - var worldBodyNode = parentNode.SelectSingleNode("worldbody") as XmlElement; - ParseBodyChildren(parentObject, worldBodyNode); + var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement; + ParseBodyChildren(rootObject, worldBodyNode); // This section references bodies, must be parsed after worldbody. - var excludeNode = parentNode.SelectSingleNode("contact") as XmlElement; + var excludeNode = mujocoNode.SelectSingleNode("contact") as XmlElement; if (excludeNode != null) { - var excludesParentObject = CreateGameObjectInParent("excludes", parentObject); + var excludesParentObject = CreateGameObjectInParent("excludes", rootObject); foreach (var child in excludeNode.OfType()) { if (child.Name != "exclude") { Debug.LogWarning( $"Only 'exclude' is supported - {child.Name} isn't supported yet.", - parentObject); + rootObject); } else { _modifiers.ApplyModifiersToElement(child); CreateGameObjectWithUniqueName(excludesParentObject, child); @@ -165,9 +160,9 @@ public class MjcfImporter { } // This section references joints/sites/geoms, must be parsed after worldbody. - var tendonNode = parentNode.SelectSingleNode("tendon") as XmlElement; + var tendonNode = mujocoNode.SelectSingleNode("tendon") as XmlElement; if (tendonNode != null) { - var tendonsParentObject = CreateGameObjectInParent("tendons", parentObject); + var tendonsParentObject = CreateGameObjectInParent("tendons", rootObject); foreach (var child in tendonNode.OfType()) { if (child.Name == "fixed") { CreateGameObjectWithUniqueName(tendonsParentObject, child); @@ -180,9 +175,9 @@ public class MjcfImporter { } // This section references worldbody elements + tendons, must be parsed after them. - var equalityNode = parentNode.SelectSingleNode("equality") as XmlElement; + var equalityNode = mujocoNode.SelectSingleNode("equality") as XmlElement; if (equalityNode != null) { - var equalitiesParentObject = CreateGameObjectInParent("equality constraints", parentObject); + var equalitiesParentObject = CreateGameObjectInParent("equality constraints", rootObject); foreach (var child in equalityNode.OfType()) { var equalityType = ParseEqualityType(child); _modifiers.ApplyModifiersToElement(child); @@ -191,9 +186,9 @@ public class MjcfImporter { } // This section references joints and tendons, must be parsed after worldbody and tendon. - var actuatorNode = parentNode.SelectSingleNode("actuator") as XmlElement; + var actuatorNode = mujocoNode.SelectSingleNode("actuator") as XmlElement; if (actuatorNode != null) { - var actuatorsParentObject = CreateGameObjectInParent("actuators", parentObject); + var actuatorsParentObject = CreateGameObjectInParent("actuators", rootObject); foreach (var child in actuatorNode.OfType()) { _modifiers.ApplyModifiersToElement(child); CreateGameObjectWithUniqueName(actuatorsParentObject, child); @@ -201,9 +196,9 @@ public class MjcfImporter { } // This section references tendons, actuators and worldbody elements, must be parsed last. - var sensorNode = parentNode.SelectSingleNode("sensor") as XmlElement; + var sensorNode = mujocoNode.SelectSingleNode("sensor") as XmlElement; if (sensorNode != null) { - var sensorParentObject = CreateGameObjectInParent("sensors", parentObject); + var sensorParentObject = CreateGameObjectInParent("sensors", rootObject); foreach (var child in sensorNode.OfType()) { _modifiers.ApplyModifiersToElement(child); var sensorType = ParseSensorType(child); diff --git a/unity/Runtime/Tools/MjcfGenerationContext.cs b/unity/Runtime/Tools/MjcfGenerationContext.cs index cdfec92b..6269bc84 100644 --- a/unity/Runtime/Tools/MjcfGenerationContext.cs +++ b/unity/Runtime/Tools/MjcfGenerationContext.cs @@ -74,7 +74,7 @@ public class MjcfGenerationContext { var settings = MjGlobalSettings.Instance; if (settings) { - settings.OptionSizeToMjcf(optionMjcf, null); + settings.GlobalsToMjcf(mjcf); } } diff --git a/unity/Tests/Editor/Components/MjGlobalSettingsTests.cs b/unity/Tests/Editor/Components/MjGlobalSettingsTests.cs index 308e1333..c8b15a9c 100644 --- a/unity/Tests/Editor/Components/MjGlobalSettingsTests.cs +++ b/unity/Tests/Editor/Components/MjGlobalSettingsTests.cs @@ -26,16 +26,13 @@ public class MjGlobalSettingsGenerationTests { private MjGlobalSettings _settings; private XmlDocument _doc; - private XmlElement _option; - private XmlElement _size; + private XmlElement _root; [SetUp] public void SetUp() { _settings = new GameObject("settings").AddComponent(); _doc = new XmlDocument(); - var root = (XmlElement)_doc.AppendChild(_doc.CreateElement("root")); - _option = (XmlElement)root.AppendChild(_doc.CreateElement("option")); - _size = (XmlElement)root.AppendChild(_doc.CreateElement("size")); + _root = (XmlElement)_doc.AppendChild(_doc.CreateElement("root")); } [TearDown] @@ -61,9 +58,9 @@ public class MjGlobalSettingsGenerationTests { _settings.GlobalOptions.NoSlipTolerance = 6.7f; _settings.GlobalOptions.MprIterations = 8; _settings.GlobalOptions.MprTolerance = 0.9f; - _settings.GlobalSizes.Nconmax = 321; + _settings.GlobalSizes.Memory = "1M"; - _settings.OptionSizeToMjcf(_option, _size); + _settings.GlobalsToMjcf(_root); Assert.That(_doc.OuterXml, Does.Contain(@"impratio=""1.2""")); Assert.That(_doc.OuterXml, Does.Contain(@"magnetic=""3.4 4.5 5.6""")); Assert.That(_doc.OuterXml, Does.Contain(@"wind=""6.7 7.8 8.9""")); @@ -89,7 +86,7 @@ public class MjGlobalSettingsGenerationTests { [Test] public void GenerateFlag() { _settings.GlobalOptions.Flag.Gravity = EnableDisableFlag.disable; - _settings.OptionSizeToMjcf(_option, _size); + _settings.GlobalsToMjcf(_root); Assert.That(_doc.OuterXml, Does.Contain(@"gravity=""disable""")); Assert.That(_doc.OuterXml, Does.Contain(@"constraint=")); Assert.That(_doc.OuterXml, Does.Contain(@"equality=")); @@ -113,18 +110,22 @@ public class MjGlobalSettingsGenerationTests { public class MjGlobalSettingsParsingTests { private MjGlobalSettings _settings; + private XmlElement _root; private XmlElement _option; private XmlElement _size; private XmlElement _flag; + private XmlElement _numeric; [SetUp] public void SetUp() { _settings = new GameObject("settings").AddComponent(); var doc = new XmlDocument(); - var root = (XmlElement)doc.AppendChild(doc.CreateElement("root")); - _option = (XmlElement)root.AppendChild(doc.CreateElement("option")); - _size = (XmlElement)root.AppendChild(doc.CreateElement("size")); + _root = (XmlElement)doc.AppendChild(doc.CreateElement("root")); + _option = (XmlElement)_root.AppendChild(doc.CreateElement("option")); + _size = (XmlElement)_root.AppendChild(doc.CreateElement("size")); _flag = (XmlElement)_option.AppendChild(doc.CreateElement("flag")); + var custom = (XmlElement)_root.AppendChild(doc.CreateElement("custom")); + _numeric = (XmlElement)custom.AppendChild(doc.CreateElement("numeric")); } [TearDown] @@ -154,9 +155,12 @@ public class MjGlobalSettingsParsingTests { _flag.SetAttribute("gravity", "disable"); - _size.SetAttribute("nconmax", "432"); + _size.SetAttribute("memory", "1M"); - _settings.ParseOptionSizeMjcf(_option, _size); + _numeric.SetAttribute("name", "numeric_name"); + _numeric.SetAttribute("data", "1 2 3"); + + _settings.ParseGlobalMjcfSections(_root); Assert.That(_settings.GlobalOptions.ImpRatio, Is.EqualTo(1.2f)); Assert.That(_settings.GlobalOptions.Magnetic, Is.EqualTo(new Vector3(3.4f, 4.5f, 5.6f))); Assert.That(_settings.GlobalOptions.Wind, Is.EqualTo(new Vector3(6.7f, 7.8f, 8.9f))); @@ -176,22 +180,11 @@ public class MjGlobalSettingsParsingTests { Assert.That(_settings.GlobalOptions.Flag.Gravity, Is.EqualTo(EnableDisableFlag.disable)); - Assert.That(_settings.GlobalSizes.Nconmax, Is.EqualTo(432)); - // these statements shouldn't fail: - _settings.ParseOptionSizeMjcf(_option, null); - _settings.ParseOptionSizeMjcf(null, _size); - } + Assert.That(_settings.GlobalSizes.Memory, Is.EqualTo("1M")); - [Test] - public void AvoidMultipleFlagClauses() { - var doubleFlagXML = @""; - var doc = new XmlDocument(); - doc.LoadXml(doubleFlagXML); - Assert.That( - () => { _settings.ParseOptionSizeMjcf(doc.DocumentElement, null); }, - Throws.ArgumentException); + Assert.That(_settings.CustomNumeric.Count, Is.EqualTo(1)); + Assert.That(_settings.CustomNumeric[0].Name, Is.EqualTo("numeric_name")); + Assert.That(_settings.CustomNumeric[0].Data, Is.EqualTo("1 2 3")); } } } diff --git a/unity/Tests/Editor/Components/MjcfGenerationContextTests.cs b/unity/Tests/Editor/Components/MjcfGenerationContextTests.cs index c1432d12..dfc908c0 100644 --- a/unity/Tests/Editor/Components/MjcfGenerationContextTests.cs +++ b/unity/Tests/Editor/Components/MjcfGenerationContextTests.cs @@ -101,18 +101,6 @@ public class MjcfGenerationContextTests { Assert.That(element.OuterXml, Does.Contain("vertex=\"1 0 0 0 0 1 0 1 0 \"")); } - [Test] - public void SettingNUserSensorValuesKeepsTheHigherValue() { - var context = new MjcfGenerationContext(); - context.NUserSensor = 5; - context.NUserSensor = 6; - context.NUserSensor = 1; - Assert.That(context.NUserSensor, Is.EqualTo(6)); - var element = new XmlDocument().CreateElement("test"); - context.GenerateMjcf(element); - Assert.That(element.OuterXml, Does.Contain("nuser_sensor=\"6\"")); - } - #region Test setup. private MjBody _componentA; diff --git a/unity/Tests/Editor/Importer/MjcfImporterTests.cs b/unity/Tests/Editor/Importer/MjcfImporterTests.cs index 74d89ed8..a4bc5a1b 100644 --- a/unity/Tests/Editor/Importer/MjcfImporterTests.cs +++ b/unity/Tests/Editor/Importer/MjcfImporterTests.cs @@ -247,27 +247,27 @@ public class MjcfImporterTests { [Test] public void ReadingSize() { var mjcfString = @" - + "; _sceneRoot = _importer.ImportString( name: string.Empty, mjcfString: mjcfString); var settings = _sceneRoot.GetComponentInChildren(); - Assert.That(settings.GlobalSizes.Njmax, Is.EqualTo(1234)); + Assert.That(settings.GlobalSizes.Memory, Is.EqualTo("1M")); } [Test] public void ReadingOptionAndSize() { var mjcfString = @" "; _sceneRoot = _importer.ImportString( name: string.Empty, mjcfString: mjcfString); var settings = _sceneRoot.GetComponentInChildren(); Assert.That(settings.GlobalOptions.ImpRatio, Is.EqualTo(5.4321f)); - Assert.That(settings.GlobalSizes.Njmax, Is.EqualTo(4321)); + Assert.That(settings.GlobalSizes.Memory, Is.EqualTo("16K")); } [Test]