From d5de67972066fb698f9194ba9d2fdaeb5c40541f Mon Sep 17 00:00:00 2001 From: Balint-H Date: Fri, 14 Jun 2024 11:38:28 +0100 Subject: [PATCH 1/5] Add draft MJCF component classes for plugin functionality --- unity/Runtime/Components/MjActuator.cs | 22 +++--- unity/Runtime/Components/Plugins.meta | 8 +++ unity/Runtime/Components/Plugins/MjPlugin.cs | 46 +++++++++++++ .../Components/Plugins/MjPlugin.cs.meta | 13 ++++ .../Components/Plugins/MjPluginConfig.cs | 48 +++++++++++++ .../Components/Plugins/MjPluginConfig.cs.meta | 13 ++++ .../Components/Plugins/MjPluginInstance.cs | 45 ++++++++++++ .../Plugins/MjPluginInstance.cs.meta | 13 ++++ .../Runtime/Components/Plugins/MjPluginTag.cs | 52 ++++++++++++++ .../Components/Plugins/MjPluginTag.cs.meta | 13 ++++ unity/Runtime/Importer/MjcfImporter.cs | 13 +++- unity/Runtime/Plugins.meta | 8 +++ unity/Runtime/Plugins/README.md | 5 ++ unity/Runtime/Plugins/README.md.meta | 7 ++ unity/Runtime/Plugins/actuator.dll.meta | 69 +++++++++++++++++++ unity/Runtime/Plugins/elasticity.dll.meta | 69 +++++++++++++++++++ unity/Runtime/Plugins/sdf.dll.meta | 27 ++++++++ unity/Runtime/Plugins/sensor.dll.meta | 27 ++++++++ unity/Runtime/Tools/MjEngineTool.cs | 8 +-- 19 files changed, 492 insertions(+), 14 deletions(-) create mode 100644 unity/Runtime/Components/Plugins.meta create mode 100644 unity/Runtime/Components/Plugins/MjPlugin.cs create mode 100644 unity/Runtime/Components/Plugins/MjPlugin.cs.meta create mode 100644 unity/Runtime/Components/Plugins/MjPluginConfig.cs create mode 100644 unity/Runtime/Components/Plugins/MjPluginConfig.cs.meta create mode 100644 unity/Runtime/Components/Plugins/MjPluginInstance.cs create mode 100644 unity/Runtime/Components/Plugins/MjPluginInstance.cs.meta create mode 100644 unity/Runtime/Components/Plugins/MjPluginTag.cs create mode 100644 unity/Runtime/Components/Plugins/MjPluginTag.cs.meta create mode 100644 unity/Runtime/Plugins.meta create mode 100644 unity/Runtime/Plugins/README.md create mode 100644 unity/Runtime/Plugins/README.md.meta create mode 100644 unity/Runtime/Plugins/actuator.dll.meta create mode 100644 unity/Runtime/Plugins/elasticity.dll.meta create mode 100644 unity/Runtime/Plugins/sdf.dll.meta create mode 100644 unity/Runtime/Plugins/sensor.dll.meta 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/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..9e4c9691c6 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPlugin.cs @@ -0,0 +1,46 @@ +// 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 MjPlugin : MjComponent { + + //Plugin identifier, used for implicit plugin instantiation. + public string Plugin = ""; + + 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) { + Plugin = mjcf.GetStringAttribute("plugin", ""); + } + + // Generate implementation specific XML element. + protected override XmlElement OnGenerateMjcf(XmlDocument doc) { + if (Plugin.Length > 0) + throw new ArgumentException($"Attribute \"plugin\" is required for {nameof(MjPlugin)}."); + var mjcf = (XmlElement)doc.CreateElement("plugin"); + mjcf.SetAttribute("plugin", Plugin); + 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..4d0ad40b96 --- /dev/null +++ b/unity/Runtime/Components/Plugins/MjPluginConfig.cs @@ -0,0 +1,48 @@ +// 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 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("plugin"); + 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..d37496ed14 --- /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 (Name.Length > 0) + throw new ArgumentException($"Attribute \"instance\" 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..ffae027b41 --- /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 { +// Actuators provide means to set joints in motion. +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/Importer/MjcfImporter.cs b/unity/Runtime/Importer/MjcfImporter.cs index 7218100705..f486a627e3 100644 --- a/unity/Runtime/Importer/MjcfImporter.cs +++ b/unity/Runtime/Importer/MjcfImporter.cs @@ -142,6 +142,16 @@ protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { settingsComponent.ParseGlobalMjcfSections(mujocoNode); } + // 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); + CreateGameObjectWithUniqueName(extensionParentObject, child, typeof(MjPlugin)); + } + } + // This makes references to assets. var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement; ParseBodyChildren(rootObject, worldBodyNode); @@ -285,10 +295,9 @@ private void ParseBodyChild(XmlElement child, GameObject parentObject) { break; } case "plugin": { - Debug.Log($"Plugin elements are only partially supported."); + CreateGameObjectWithUniqueName(parentObject, child); break; } - default: { Debug.Log($"The importer does not yet support tags <{child.Name}>."); break; 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..0af11116e6 --- /dev/null +++ b/unity/Runtime/Plugins/README.md @@ -0,0 +1,5 @@ +## User MuJoCo Plugins + +MuJoCo physics functionality can be extended with user plugins. For more information, see the [documentation ](https://mujoco.readthedocs.io/en/latest/programming/extension.html#explugin). Place plugin libraries in this folder to load them when importing or running a model with user plugins. + +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/Plugins/actuator.dll.meta b/unity/Runtime/Plugins/actuator.dll.meta new file mode 100644 index 0000000000..5a860a408e --- /dev/null +++ b/unity/Runtime/Plugins/actuator.dll.meta @@ -0,0 +1,69 @@ +fileFormatVersion: 2 +guid: ca0af2df9b7bb3e49b34f2e17dc93fc9 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 1 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 0 + Exclude OSXUniversal: 0 + Exclude WebGL: 0 + Exclude Win: 0 + Exclude Win64: 0 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Plugins/elasticity.dll.meta b/unity/Runtime/Plugins/elasticity.dll.meta new file mode 100644 index 0000000000..c49c684096 --- /dev/null +++ b/unity/Runtime/Plugins/elasticity.dll.meta @@ -0,0 +1,69 @@ +fileFormatVersion: 2 +guid: e2b519f4c71095445bdddfa03d7aba0b +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 1 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 0 + Exclude OSXUniversal: 0 + Exclude WebGL: 0 + Exclude Win: 0 + Exclude Win64: 0 + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win64 + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Plugins/sdf.dll.meta b/unity/Runtime/Plugins/sdf.dll.meta new file mode 100644 index 0000000000..47c17fa7d7 --- /dev/null +++ b/unity/Runtime/Plugins/sdf.dll.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 23465aea1d292384e97038b4d7e772a2 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Plugins/sensor.dll.meta b/unity/Runtime/Plugins/sensor.dll.meta new file mode 100644 index 0000000000..e427b19a8a --- /dev/null +++ b/unity/Runtime/Plugins/sensor.dll.meta @@ -0,0 +1,27 @@ +fileFormatVersion: 2 +guid: 0ea6a69f9bb9f554a90ba5f7a8fcf4f5 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Tools/MjEngineTool.cs b/unity/Runtime/Tools/MjEngineTool.cs index bf27a1d950..dad2df1f7f 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; @@ -372,12 +373,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 { From c017eb4c5936ccebb72f8c0cc71b75e93f5090fe Mon Sep 17 00:00:00 2001 From: Balint-H Date: Thu, 14 Nov 2024 17:11:33 +0000 Subject: [PATCH 2/5] Remove dll meta files --- .gitignore | 1 + unity/Runtime/Plugins/actuator.dll.meta | 69 ----------------------- unity/Runtime/Plugins/elasticity.dll.meta | 69 ----------------------- unity/Runtime/Plugins/sdf.dll.meta | 27 --------- unity/Runtime/Plugins/sensor.dll.meta | 27 --------- 5 files changed, 1 insertion(+), 192 deletions(-) delete mode 100644 unity/Runtime/Plugins/actuator.dll.meta delete mode 100644 unity/Runtime/Plugins/elasticity.dll.meta delete mode 100644 unity/Runtime/Plugins/sdf.dll.meta delete mode 100644 unity/Runtime/Plugins/sensor.dll.meta diff --git a/.gitignore b/.gitignore index c55f0c71cd..f7075cf9bf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.o *.so *.so.* +*.dll.meta # Exclude editor config .vscode/ diff --git a/unity/Runtime/Plugins/actuator.dll.meta b/unity/Runtime/Plugins/actuator.dll.meta deleted file mode 100644 index 5a860a408e..0000000000 --- a/unity/Runtime/Plugins/actuator.dll.meta +++ /dev/null @@ -1,69 +0,0 @@ -fileFormatVersion: 2 -guid: ca0af2df9b7bb3e49b34f2e17dc93fc9 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 1 - isOverridable: 1 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Editor: 0 - Exclude Linux64: 0 - Exclude OSXUniversal: 0 - Exclude WebGL: 0 - Exclude Win: 0 - Exclude Win64: 0 - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: AnyCPU - DefaultValueInitialized: true - OS: AnyOS - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Win - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - WebGL: WebGL - second: - enabled: 1 - settings: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/Runtime/Plugins/elasticity.dll.meta b/unity/Runtime/Plugins/elasticity.dll.meta deleted file mode 100644 index c49c684096..0000000000 --- a/unity/Runtime/Plugins/elasticity.dll.meta +++ /dev/null @@ -1,69 +0,0 @@ -fileFormatVersion: 2 -guid: e2b519f4c71095445bdddfa03d7aba0b -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 1 - isOverridable: 1 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - : Any - second: - enabled: 0 - settings: - Exclude Editor: 0 - Exclude Linux64: 0 - Exclude OSXUniversal: 0 - Exclude WebGL: 0 - Exclude Win: 0 - Exclude Win64: 0 - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 1 - settings: - CPU: AnyCPU - DefaultValueInitialized: true - OS: AnyOS - - first: - Standalone: Linux64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: OSXUniversal - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Win - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - Standalone: Win64 - second: - enabled: 1 - settings: - CPU: AnyCPU - - first: - WebGL: WebGL - second: - enabled: 1 - settings: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/Runtime/Plugins/sdf.dll.meta b/unity/Runtime/Plugins/sdf.dll.meta deleted file mode 100644 index 47c17fa7d7..0000000000 --- a/unity/Runtime/Plugins/sdf.dll.meta +++ /dev/null @@ -1,27 +0,0 @@ -fileFormatVersion: 2 -guid: 23465aea1d292384e97038b4d7e772a2 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 1 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - userData: - assetBundleName: - assetBundleVariant: diff --git a/unity/Runtime/Plugins/sensor.dll.meta b/unity/Runtime/Plugins/sensor.dll.meta deleted file mode 100644 index e427b19a8a..0000000000 --- a/unity/Runtime/Plugins/sensor.dll.meta +++ /dev/null @@ -1,27 +0,0 @@ -fileFormatVersion: 2 -guid: 0ea6a69f9bb9f554a90ba5f7a8fcf4f5 -PluginImporter: - externalObjects: {} - serializedVersion: 2 - iconMap: {} - executionOrder: {} - defineConstraints: [] - isPreloaded: 0 - isOverridable: 1 - isExplicitlyReferenced: 0 - validateReferences: 1 - platformData: - - first: - Any: - second: - enabled: 1 - settings: {} - - first: - Editor: Editor - second: - enabled: 0 - settings: - DefaultValueInitialized: true - userData: - assetBundleName: - assetBundleVariant: From d5d8b0f08f1c9f8e3fcaa4a03115102ea6d0a999 Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 3 Dec 2024 17:29:30 +0000 Subject: [PATCH 3/5] Add Custom element --- unity/Runtime/Components/Custom.meta | 8 ++ unity/Runtime/Components/Custom/MjCustom.cs | 115 ++++++++++++++++++ .../Components/Custom/MjCustom.cs.meta | 11 ++ unity/Runtime/Components/MjScene.cs | 13 +- unity/Runtime/Components/Plugins/MjPlugin.cs | 16 ++- .../Components/Plugins/MjPluginConfig.cs | 1 - .../Runtime/Components/Plugins/MjPluginTag.cs | 2 +- unity/Runtime/Importer/MjcfImporter.cs | 53 +++++++- unity/Runtime/Plugins/README.md | 12 +- 9 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 unity/Runtime/Components/Custom.meta create mode 100644 unity/Runtime/Components/Custom/MjCustom.cs create mode 100644 unity/Runtime/Components/Custom/MjCustom.cs.meta 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..1247b556a7 --- /dev/null +++ b/unity/Runtime/Components/Custom/MjCustom.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Xml; +using UnityEngine; + +namespace Mujoco { +public class MjCustom : MonoBehaviour { + public void ParseCustom(GameObject parentObject, XmlElement parentNode) { + foreach (XmlElement child in parentNode.ChildNodes) { + switch (child.Name) { + case "numeric": + var numeric = new MjNumeric(); + numeric.Parse(child); + break; + + case "text": + var text = new MjText(); + text.Parse(child); + break; + + case "tuple": + var tuple = new MjTuple(); + tuple.Parse(child); + break; + + default: + Debug.LogWarning($"Unknown custom element: {child.Name}"); + break; + } + } + } + + private class MjText { + public string Name { get; private set; } + + public string Data { get; private set; } + + public void Parse(XmlElement mjcf) { + Name = mjcf.GetStringAttribute("name"); + Data = mjcf.GetStringAttribute("data"); + } + } + + private class MjNumeric { + public string Name { get; private set; } + + public int Size { get; private set; } + + public float[] Data { get; private set; } + + public void Parse(XmlElement mjcf) { + Name = mjcf.GetStringAttribute("name"); + + if (int.TryParse(mjcf.GetStringAttribute("size", "-1"), out int size)) { + Size = size; + } + var data = mjcf.GetFloatArrayAttribute("data", defaultValue: new float[]{}); + + if (Size>-1 && data.Length != Size) { + Array.Resize(ref data, Size); + } + Data = data; + Size = Data.Length; + } + } + + private class MjTuple { + public string Name { get; private set; } + + public List Elements { get; private set; } = new List(); + + public void Parse(XmlElement element) { + Name = element.GetAttribute("name"); + if (string.IsNullOrEmpty(Name)) { + throw new ArgumentException("MjTuple element must have a 'name' attribute."); + } + + foreach (XmlElement child in element.ChildNodes) { + if (child.Name == "element") { + var tupleElement = new TupleElement(); + tupleElement.Parse(child); + Elements.Add(tupleElement); + } else { + Debug.LogWarning($"Unknown child element in MjTuple: {child.Name}"); + } + } + } + + public class TupleElement { + public string ObjType { get; private set; } + + public string ObjName { get; private set; } + + public double Prm { get; private set; } + + public void Parse(XmlElement element) { + ObjType = element.GetAttribute("objtype"); + if (string.IsNullOrEmpty(ObjType)) { + throw new ArgumentException("Tuple element must have an 'objtype' attribute."); + } + + ObjName = element.GetAttribute("objname"); + if (string.IsNullOrEmpty(ObjName)) { + throw new ArgumentException("Tuple element must have an 'objname' attribute."); + } + + if (double.TryParse(element.GetAttribute("prm"), out double prm)) { + Prm = prm; + } + } + } + } +} +} \ 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/MjScene.cs b/unity/Runtime/Components/MjScene.cs index c55c5cf52c..8840e25f42 100644 --- a/unity/Runtime/Components/MjScene.cs +++ b/unity/Runtime/Components/MjScene.cs @@ -393,9 +393,20 @@ 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 extensionMjcf = (XmlElement)MjRoot.AppendChild(doc.CreateElement("extension")); + BuildHierarchicalMjcf( + doc, + components.Where(component => + (component is MjPlugin) || + (component is MjPluginInstance) || + (component is MjPluginConfig)), + extensionMjcf); + // Non-hierarchical sections: MjRoot.AppendChild(GenerateMjcfSection( doc, components.Where(component => component is MjExclude), "contact")); diff --git a/unity/Runtime/Components/Plugins/MjPlugin.cs b/unity/Runtime/Components/Plugins/MjPlugin.cs index 9e4c9691c6..96618625c1 100644 --- a/unity/Runtime/Components/Plugins/MjPlugin.cs +++ b/unity/Runtime/Components/Plugins/MjPlugin.cs @@ -19,27 +19,33 @@ using UnityEngine; namespace Mujoco { -// Actuators provide means to set joints in motion. + 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_UNKNOWN; + 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) { - if (Plugin.Length > 0) - throw new ArgumentException($"Attribute \"plugin\" is required for {nameof(MjPlugin)}."); + var mjcf = (XmlElement)doc.CreateElement("plugin"); - mjcf.SetAttribute("plugin", 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/MjPluginConfig.cs b/unity/Runtime/Components/Plugins/MjPluginConfig.cs index 4d0ad40b96..239d9f1061 100644 --- a/unity/Runtime/Components/Plugins/MjPluginConfig.cs +++ b/unity/Runtime/Components/Plugins/MjPluginConfig.cs @@ -19,7 +19,6 @@ using UnityEngine; namespace Mujoco { -// Actuators provide means to set joints in motion. public class MjPluginConfig : MjComponent { public string Key = ""; public string Value = ""; diff --git a/unity/Runtime/Components/Plugins/MjPluginTag.cs b/unity/Runtime/Components/Plugins/MjPluginTag.cs index ffae027b41..552fa2ce71 100644 --- a/unity/Runtime/Components/Plugins/MjPluginTag.cs +++ b/unity/Runtime/Components/Plugins/MjPluginTag.cs @@ -19,7 +19,7 @@ using UnityEngine; namespace Mujoco { -// Actuators provide means to set joints in motion. + public class MjPluginTag : MjComponent { //Plugin identifier, used for implicit plugin instantiation. diff --git a/unity/Runtime/Importer/MjcfImporter.cs b/unity/Runtime/Importer/MjcfImporter.cs index f486a627e3..b12b4e48cb 100644 --- a/unity/Runtime/Importer/MjcfImporter.cs +++ b/unity/Runtime/Importer/MjcfImporter.cs @@ -148,7 +148,7 @@ protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { var extensionParentObject = CreateGameObjectInParent("extension", rootObject); foreach (var child in extensionNode.OfType()) { _modifiers.ApplyModifiersToElement(child); - CreateGameObjectWithUniqueName(extensionParentObject, child, typeof(MjPlugin)); + ParseBodyChildren(extensionParentObject, extensionNode); } } @@ -249,6 +249,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) { @@ -295,7 +331,20 @@ private void ParseBodyChild(XmlElement child, GameObject parentObject) { break; } case "plugin": { - CreateGameObjectWithUniqueName(parentObject, child); + 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: { diff --git a/unity/Runtime/Plugins/README.md b/unity/Runtime/Plugins/README.md index 0af11116e6..53b28c2663 100644 --- a/unity/Runtime/Plugins/README.md +++ b/unity/Runtime/Plugins/README.md @@ -1,5 +1,13 @@ -## User MuJoCo Plugins +## User MuJoCo Plug-ins -MuJoCo physics functionality can be extended with user plugins. For more information, see the [documentation ](https://mujoco.readthedocs.io/en/latest/programming/extension.html#explugin). Place plugin libraries in this folder to load them when importing or running a model with user plugins. +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 From 7990081ad0eab5ba7f5260e425e419da277c62be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Tue, 10 Dec 2024 10:55:31 +0000 Subject: [PATCH 4/5] Implement custom objects --- unity/Runtime/Components/Custom/MjCustom.cs | 214 ++++++++++++++---- .../Runtime/Components/Equality/MjConnect.cs | 27 ++- unity/Runtime/Components/MjScene.cs | 4 + .../Components/Plugins/MjPluginConfig.cs | 2 +- .../Components/Plugins/MjPluginInstance.cs | 4 +- unity/Runtime/Importer/MjcfImporter.cs | 16 +- 6 files changed, 207 insertions(+), 60 deletions(-) diff --git a/unity/Runtime/Components/Custom/MjCustom.cs b/unity/Runtime/Components/Custom/MjCustom.cs index 1247b556a7..bb0bc18608 100644 --- a/unity/Runtime/Components/Custom/MjCustom.cs +++ b/unity/Runtime/Components/Custom/MjCustom.cs @@ -3,112 +3,236 @@ using System.Collections.Generic; using System.Xml; using UnityEngine; +using UnityEngine.Serialization; namespace Mujoco { public class MjCustom : MonoBehaviour { - public void ParseCustom(GameObject parentObject, XmlElement parentNode) { - foreach (XmlElement child in parentNode.ChildNodes) { + + [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); } } - private class MjText { - public string Name { get; private set; } - public string Data { get; private set; } + [Serializable] + private abstract class MjCustomElement { + + [SerializeField] + public string name; public void Parse(XmlElement mjcf) { - Name = mjcf.GetStringAttribute("name"); - Data = mjcf.GetStringAttribute("data"); + 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); + + } - private class MjNumeric { - public string Name { get; private set; } + [Serializable] + private class MjText : MjCustomElement { - public int Size { get; private set; } + [SerializeField] + public string data; - public float[] Data { get; private set; } + protected override void ParseInner(XmlElement mjcf) { + data = mjcf.GetStringAttribute("data"); + } - public void Parse(XmlElement mjcf) { - Name = mjcf.GetStringAttribute("name"); + protected override XmlElement ToMjcfInner(XmlDocument doc) { + var mjcf = (XmlElement)doc.CreateElement("numeric"); - if (int.TryParse(mjcf.GetStringAttribute("size", "-1"), out int size)) { - Size = size; - } - var data = mjcf.GetFloatArrayAttribute("data", defaultValue: new float[]{}); + mjcf.SetAttribute("data", data); - if (Size>-1 && data.Length != Size) { - Array.Resize(ref data, Size); - } - Data = data; - Size = Data.Length; + return mjcf; } } - private class MjTuple { - public string Name { get; private set; } - public List Elements { get; private set; } = new List(); + [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[] { }); - public void Parse(XmlElement element) { - Name = element.GetAttribute("name"); - if (string.IsNullOrEmpty(Name)) { - throw new ArgumentException("MjTuple element must have a 'name' attribute."); + 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 { - foreach (XmlElement child in element.ChildNodes) { + [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); - Elements.Add(tupleElement); + 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; } - public class TupleElement { - public string ObjType { get; private set; } + [Serializable] + protected class TupleElement { + + [SerializeField] + public string objType; - public string ObjName { get; private set; } + [SerializeField] + public string objName; - public double Prm { get; private set; } + [SerializeField] + public float prm; public void Parse(XmlElement element) { - ObjType = element.GetAttribute("objtype"); - if (string.IsNullOrEmpty(ObjType)) { - throw new ArgumentException("Tuple element must have an 'objtype' attribute."); - } + objType = element.GetAttribute("objtype"); + objName = element.GetAttribute("objname"); - ObjName = element.GetAttribute("objname"); - if (string.IsNullOrEmpty(ObjName)) { - throw new ArgumentException("Tuple element must have an 'objname' attribute."); - } + element.GetFloatAttribute("prm", float.NaN); + } - if (double.TryParse(element.GetAttribute("prm"), out double prm)) { - Prm = prm; + 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; } + } } } 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/MjScene.cs b/unity/Runtime/Components/MjScene.cs index 8840e25f42..dcee775285 100644 --- a/unity/Runtime/Components/MjScene.cs +++ b/unity/Runtime/Components/MjScene.cs @@ -406,6 +406,10 @@ private XmlDocument GenerateSceneMjcf(IEnumerable components) { (component is MjPluginInstance) || (component is MjPluginConfig)), extensionMjcf); + + if (MjCustom.InstanceExists) { + MjCustom.Instance.GenerateCustomMjcf(doc); + } // Non-hierarchical sections: MjRoot.AppendChild(GenerateMjcfSection( diff --git a/unity/Runtime/Components/Plugins/MjPluginConfig.cs b/unity/Runtime/Components/Plugins/MjPluginConfig.cs index 239d9f1061..159c056214 100644 --- a/unity/Runtime/Components/Plugins/MjPluginConfig.cs +++ b/unity/Runtime/Components/Plugins/MjPluginConfig.cs @@ -36,7 +36,7 @@ protected override void OnParseMjcf(XmlElement mjcf) { // Generate implementation specific XML element. protected override XmlElement OnGenerateMjcf(XmlDocument doc) { - var mjcf = (XmlElement)doc.CreateElement("plugin"); + var mjcf = (XmlElement)doc.CreateElement("config"); if (Key.Length > 0) mjcf.SetAttribute("key", Key); if (Value.Length > 0) diff --git a/unity/Runtime/Components/Plugins/MjPluginInstance.cs b/unity/Runtime/Components/Plugins/MjPluginInstance.cs index d37496ed14..a670fbdade 100644 --- a/unity/Runtime/Components/Plugins/MjPluginInstance.cs +++ b/unity/Runtime/Components/Plugins/MjPluginInstance.cs @@ -35,8 +35,8 @@ protected override void OnParseMjcf(XmlElement mjcf) { // Generate implementation specific XML element. protected override XmlElement OnGenerateMjcf(XmlDocument doc) { - if (Name.Length > 0) - throw new ArgumentException($"Attribute \"instance\" is required for {nameof(MjPluginInstance)}."); + 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/Importer/MjcfImporter.cs b/unity/Runtime/Importer/MjcfImporter.cs index b12b4e48cb..9f2b52dc51 100644 --- a/unity/Runtime/Importer/MjcfImporter.cs +++ b/unity/Runtime/Importer/MjcfImporter.cs @@ -148,12 +148,22 @@ protected virtual void ParseRoot(GameObject rootObject, XmlElement mujocoNode) { var extensionParentObject = CreateGameObjectInParent("extension", rootObject); foreach (var child in extensionNode.OfType()) { _modifiers.ApplyModifiersToElement(child); - ParseBodyChildren(extensionParentObject, extensionNode); + ParseExtensions(extensionParentObject, extensionNode); } } - // This makes references to assets. - var worldBodyNode = mujocoNode.SelectSingleNode("worldbody") as XmlElement; + 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. From 28e042a7e7b1e4ed68a2e5696270cc532775c9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Fri, 3 Jan 2025 16:14:57 +0000 Subject: [PATCH 5/5] Add support for flex bodies --- .../Components/MjFlexDeformableEditor.cs | 67 +++++ .../Components/MjFlexDeformableEditor.cs.meta | 11 + unity/Runtime/Components/Deformable.meta | 8 + .../Components/Deformable/MjFlexDeformable.cs | 241 ++++++++++++++++++ .../Deformable/MjFlexDeformable.cs.meta | 11 + .../Deformable/MjFlexMeshBuilder.cs | 123 +++++++++ .../Deformable/MjFlexMeshBuilder.cs.meta | 11 + .../Components/Equality/MjFlexConstraint.cs | 35 +++ .../Equality/MjFlexConstraint.cs.meta | 11 + unity/Runtime/Components/Equality/MjWeld.cs | 13 +- unity/Runtime/Components/MjGlobalSettings.cs | 6 +- unity/Runtime/Components/MjScene.cs | 22 +- .../Components/Shapes/MjGeomSettings.cs | 10 +- unity/Runtime/Importer/MjcfImporter.cs | 33 ++- unity/Runtime/Tools/MjEngineTool.cs | 21 +- unity/Runtime/Tools/XmlElementExtensions.cs | 57 ++++- 16 files changed, 654 insertions(+), 26 deletions(-) create mode 100644 unity/Editor/Components/MjFlexDeformableEditor.cs create mode 100644 unity/Editor/Components/MjFlexDeformableEditor.cs.meta create mode 100644 unity/Runtime/Components/Deformable.meta create mode 100644 unity/Runtime/Components/Deformable/MjFlexDeformable.cs create mode 100644 unity/Runtime/Components/Deformable/MjFlexDeformable.cs.meta create mode 100644 unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs create mode 100644 unity/Runtime/Components/Deformable/MjFlexMeshBuilder.cs.meta create mode 100644 unity/Runtime/Components/Equality/MjFlexConstraint.cs create mode 100644 unity/Runtime/Components/Equality/MjFlexConstraint.cs.meta 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/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/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/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 dcee775285..72a81e0398 100644 --- a/unity/Runtime/Components/MjScene.cs +++ b/unity/Runtime/Components/MjScene.cs @@ -397,15 +397,15 @@ private XmlDocument GenerateSceneMjcf(IEnumerable components) { (component is MjPluginTag)), worldMjcf); - //MuJoCo plug-ins have some hierarchical structure too - var extensionMjcf = (XmlElement)MjRoot.AppendChild(doc.CreateElement("extension")); - BuildHierarchicalMjcf( - doc, - components.Where(component => - (component is MjPlugin) || - (component is MjPluginInstance) || - (component is MjPluginConfig)), - extensionMjcf); + //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); @@ -418,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/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 9f2b52dc51..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 { @@ -198,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()) { @@ -379,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/Tools/MjEngineTool.cs b/unity/Runtime/Tools/MjEngineTool.cs index dad2df1f7f..cc1e7f1242 100644 --- a/unity/Runtime/Tools/MjEngineTool.cs +++ b/unity/Runtime/Tools/MjEngineTool.cs @@ -245,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. 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; + } } }