From 36cbef50b70b2d733f5f1072d3ad4a0f5b96482f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Mon, 20 Jan 2025 11:09:18 +0000 Subject: [PATCH 1/3] Allow loading obj meshes --- unity/Editor/Importer/MjImporterWithAssets.cs | 26 +++-- unity/Editor/Importer/ObjMeshImportUtility.cs | 94 +++++++++++++++++++ .../Runtime/Components/Shapes/MjMeshShape.cs | 2 +- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 unity/Editor/Importer/ObjMeshImportUtility.cs diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index a9a93184d6..9937ce5765 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -153,20 +153,21 @@ private void ParseMesh(XmlElement parentNode) { var assetReferenceName = MjEngineTool.Sanitize(unsanitizedAssetReferenceName); var sourceFilePath = Path.Combine(_sourceMeshesDir, fileName); - if (Path.GetExtension(sourceFilePath) == ".obj") { - throw new NotImplementedException("OBJ mesh file loading is not yet implemented. " + - "Please convert to binary STL. " + + if (Path.GetExtension(sourceFilePath) != ".obj" && Path.GetExtension(sourceFilePath) != ".stl") { + throw new NotImplementedException("Type of mesh file not yet supported. " + + "Please convert to binary STL or OBJ. " + $"Attempted to load: {sourceFilePath}"); } - var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName + ".stl"); + var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName + + Path.GetExtension(sourceFilePath)); if (File.Exists(targetFilePath)) { File.Delete(targetFilePath); } var scale = MjEngineTool.UnityVector3( parentNode.GetVector3Attribute("scale", defaultValue: Vector3.one)); CopyMeshAndRescale(sourceFilePath, targetFilePath, scale); - var assetPath = Path.Combine(_targetAssetDir, assetReferenceName + ".stl"); + var assetPath = Path.Combine(_targetAssetDir, assetReferenceName + Path.GetExtension(sourceFilePath)); // This asset path should be available because the MuJoCo compiler guarantees element names // are unique, but check for completeness (and in case sanitizing the name broke uniqueness): if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) { @@ -174,7 +175,7 @@ private void ParseMesh(XmlElement parentNode) { $"Trying to import mesh {unsanitizedAssetReferenceName} but {assetPath} already exists."); } AssetDatabase.ImportAsset(assetPath); - var copiedMesh = AssetDatabase.LoadMainAssetAtPath(assetPath) as Mesh; + var copiedMesh = AssetDatabase.LoadAssetAtPath(assetPath); if (copiedMesh == null) { throw new Exception($"Mesh {assetPath} was not imported."); } @@ -186,9 +187,16 @@ private void ParseMesh(XmlElement parentNode) { private void CopyMeshAndRescale( string sourceFilePath, string targetFilePath, Vector3 scale) { var originalMeshBytes = File.ReadAllBytes(sourceFilePath); - var mesh = StlMeshParser.ParseBinary(originalMeshBytes, scale); - var rescaledMeshBytes = StlMeshParser.SerializeBinary(mesh); - File.WriteAllBytes(targetFilePath, rescaledMeshBytes); + if (Path.GetExtension(sourceFilePath) == ".stl") { + var mesh = StlMeshParser.ParseBinary(originalMeshBytes, scale); + var rescaledMeshBytes = StlMeshParser.SerializeBinary(mesh); + File.WriteAllBytes(targetFilePath, rescaledMeshBytes); + } else if (Path.GetExtension(sourceFilePath) == ".obj") { + ObjMeshImportUtility.CopyAndScaleOBJFile(sourceFilePath, targetFilePath, scale); + } else { + throw new NotImplementedException($"Extension {Path.GetExtension(sourceFilePath)} " + + $"not yet supported for MuJoCo mesh asset."); + } } private void ParseMaterial(XmlElement parentNode) { diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs new file mode 100644 index 0000000000..3e2e988563 --- /dev/null +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -0,0 +1,94 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Mujoco { + + /// + /// Scale vertex data manually line by line. We skip normals. Parameter vertex points + /// (`vp`) was unclear to me how to properly scale them, if you use them and notice an + /// issue please report it. + /// + public static class ObjMeshImportUtility { + private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); + + public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, + Vector3 scale, bool flipFaces=false) { + // OBJ files are human readable + string[] lines = File.ReadAllLines(sourceFilePath); + StringBuilder outputBuilder = new StringBuilder(); + // Culture info for consistent decimal point handling + CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + scale = ToXZY(scale.x, scale.y, scale.z); + foreach (string line in lines) { + if (line.StartsWith("v ")) // Vertex line + { + // Split the line into components + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + // Scale the vertex + float x = -float.Parse(parts[1], invariantCulture) * scale.x; + float y = float.Parse(parts[2], invariantCulture) * scale.y; + float z = float.Parse(parts[3], invariantCulture) * scale.z; + + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"v {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("vn ")) { + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + float x = -float.Parse(parts[1], invariantCulture); + float y = float.Parse(parts[2], invariantCulture); + float z = float.Parse(parts[3], invariantCulture); + + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"vn {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("f ") && flipFaces) { + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + outputBuilder.Append(parts[0] + " "); + + // Apply same vertex order as STL parser: [0,2,1] + var face = parts.Skip(1).ToArray(); + if (face.Length >= 3) { + outputBuilder.Append(face[0] + " "); // vertex 0 + outputBuilder.Append(face[2] + " "); // vertex 2 + outputBuilder.Append(face[1]); // vertex 1 + + // Append any remaining vertices in original order + for (int i = 3; i < face.Length; i++) { + outputBuilder.Append(" " + face[i]); + } + } + outputBuilder.AppendLine(); + } + } else { + // Copy non-vertex lines as-is + outputBuilder.AppendLine(line); + } + } + // Write the scaled OBJ to the target file + File.WriteAllText(targetFilePath, outputBuilder.ToString()); + } + } +} \ No newline at end of file diff --git a/unity/Runtime/Components/Shapes/MjMeshShape.cs b/unity/Runtime/Components/Shapes/MjMeshShape.cs index e4fa338d0f..22721209da 100644 --- a/unity/Runtime/Components/Shapes/MjMeshShape.cs +++ b/unity/Runtime/Components/Shapes/MjMeshShape.cs @@ -36,7 +36,7 @@ public void FromMjcf(XmlElement mjcf) { var assetName = MjEngineTool.Sanitize( mjcf.GetStringAttribute("mesh", defaultValue: string.Empty)); if (!string.IsNullOrEmpty(assetName)) { - Mesh = (Mesh)Resources.Load(assetName); + Mesh = Resources.Load(assetName); } } From a58f8d0033bfad79aa159ba3f11e6d16f988a21c Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 21 Jan 2025 15:11:27 +0000 Subject: [PATCH 2/3] Add OBJ mesh support to Unity plugin --- unity/Editor/Importer/MjImporterWithAssets.cs | 16 ++- unity/Editor/Importer/ObjMeshImportUtility.cs | 127 +++++++++--------- .../Importer/ObjMeshImportUtility.cs.meta | 11 ++ .../Runtime/Components/Shapes/MjMeshFilter.cs | 14 +- 4 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 unity/Editor/Importer/ObjMeshImportUtility.cs.meta diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index 9937ce5765..4ece4caeb0 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -174,7 +174,14 @@ private void ParseMesh(XmlElement parentNode) { throw new Exception( $"Trying to import mesh {unsanitizedAssetReferenceName} but {assetPath} already exists."); } + AssetDatabase.ImportAsset(assetPath); + ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter; + if (importer != null && !importer.isReadable) { + importer.isReadable = true; + importer.SaveAndReimport(); + } + var copiedMesh = AssetDatabase.LoadAssetAtPath(assetPath); if (copiedMesh == null) { throw new Exception($"Mesh {assetPath} was not imported."); @@ -283,12 +290,12 @@ private void ResolveOrCreateMaterial(MeshRenderer renderer, XmlElement parentNod // We use the geom's name, guaranteed to be unique, as the asset name. // If geom is nameless, use a random number. var name = - MjEngineTool.Sanitize(parentNode.GetStringAttribute( - "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); - var assetPath = Path.Combine(_targetAssetDir, name + ".mat"); + MjEngineTool.Sanitize(parentNode.GetStringAttribute( + "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); + var assetPath = Path.Combine(_targetAssetDir, name+".mat"); if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) { throw new Exception( - $"Creating a material asset for the geom {name}, but {assetPath} already exists."); + $"Creating a material asset for the geom {name}, but {assetPath} already exists."); } AssetDatabase.CreateAsset(material, assetPath); AssetDatabase.SaveAssets(); @@ -297,6 +304,7 @@ private void ResolveOrCreateMaterial(MeshRenderer renderer, XmlElement parentNod material = DefaultMujocoMaterial; } } + if (parentNode.GetFloatAttribute("group") > 2) renderer.enabled = false; renderer.sharedMaterial = material; } } diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs index 3e2e988563..2eea221113 100644 --- a/unity/Editor/Importer/ObjMeshImportUtility.cs +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -11,84 +11,85 @@ // 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.Globalization; using System.IO; using System.Linq; using System.Text; +using UnityEditor; using UnityEngine; namespace Mujoco { - /// - /// Scale vertex data manually line by line. We skip normals. Parameter vertex points - /// (`vp`) was unclear to me how to properly scale them, if you use them and notice an - /// issue please report it. - /// - public static class ObjMeshImportUtility { - private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); - - public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, - Vector3 scale, bool flipFaces=false) { - // OBJ files are human readable - string[] lines = File.ReadAllLines(sourceFilePath); - StringBuilder outputBuilder = new StringBuilder(); - // Culture info for consistent decimal point handling - CultureInfo invariantCulture = CultureInfo.InvariantCulture; - - scale = ToXZY(scale.x, scale.y, scale.z); - foreach (string line in lines) { - if (line.StartsWith("v ")) // Vertex line - { - // Split the line into components - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - // Scale the vertex - float x = -float.Parse(parts[1], invariantCulture) * scale.x; - float y = float.Parse(parts[2], invariantCulture) * scale.y; - float z = float.Parse(parts[3], invariantCulture) * scale.z; - - var swizzled = ToXZY(x, y, z); - outputBuilder.AppendLine( - $"v {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); - } - } else if (line.StartsWith("vn ")) { - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - float x = -float.Parse(parts[1], invariantCulture); - float y = float.Parse(parts[2], invariantCulture); - float z = float.Parse(parts[3], invariantCulture); +/// +/// Scale vertex data manually line by line. We skip normals. Warning: Parameter vertex points +/// (`vp`) were unclear to me how to handle with scaling, if you use them and notice an issue +/// please report it. +/// +public static class ObjMeshImportUtility { + private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); - var swizzled = ToXZY(x, y, z); - outputBuilder.AppendLine( - $"vn {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); - } - } else if (line.StartsWith("f ") && flipFaces) { - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - outputBuilder.Append(parts[0] + " "); + public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, + Vector3 scale) { + // OBJ files are human readable + string[] lines = File.ReadAllLines(sourceFilePath); + StringBuilder outputBuilder = new StringBuilder(); + // Culture info for consistent decimal point handling + CultureInfo invariantCulture = CultureInfo.InvariantCulture; + scale = ToXZY(scale.x, scale.y, scale.z); + foreach (string line in lines) { + if (line.StartsWith("v ")) // Vertex line + { + // Split the line into components + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + // Scale the vertex. It is unclear to me why flipping along x axis was necessary, + // but without it meshes were incorrectly oriented. + float x = -float.Parse(parts[1], invariantCulture) * scale.x; + float y = float.Parse(parts[2], invariantCulture) * scale.y; + float z = float.Parse(parts[3], invariantCulture) * scale.z; - // Apply same vertex order as STL parser: [0,2,1] - var face = parts.Skip(1).ToArray(); - if (face.Length >= 3) { - outputBuilder.Append(face[0] + " "); // vertex 0 - outputBuilder.Append(face[2] + " "); // vertex 2 - outputBuilder.Append(face[1]); // vertex 1 + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"v {swizzled.x.ToString(invariantCulture)} "+ + $"{swizzled.y.ToString(invariantCulture)} "+ + $"{swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("vn ")) { + // We swizzle the normals too + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + float x = -float.Parse(parts[1], invariantCulture); + float y = float.Parse(parts[2], invariantCulture); + float z = float.Parse(parts[3], invariantCulture); - // Append any remaining vertices in original order - for (int i = 3; i < face.Length; i++) { - outputBuilder.Append(" " + face[i]); - } - } - outputBuilder.AppendLine(); + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"vn {swizzled.x.ToString(invariantCulture)} "+ + $"{swizzled.y.ToString(invariantCulture)} "+ + $"{swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("f ") && scale.x*scale.y*scale.z < 0) { + // Faces definition, flip face by reordering vertices + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + outputBuilder.Append(parts[0]+" "); + var face = parts.Skip(1).ToArray(); + if (face.Length >= 3) { + outputBuilder.Append(face[0]+" "); + outputBuilder.Append(face[2]+" "); + outputBuilder.Append(face[1]); } - } else { - // Copy non-vertex lines as-is - outputBuilder.AppendLine(line); + outputBuilder.AppendLine(); } + } else { + // Copy non-vertex lines as-is + outputBuilder.AppendLine(line); } - // Write the scaled OBJ to the target file - File.WriteAllText(targetFilePath, outputBuilder.ToString()); } + // Write the scaled OBJ to the target file + File.WriteAllText(targetFilePath, outputBuilder.ToString()); } +} } \ No newline at end of file diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs.meta b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta new file mode 100644 index 0000000000..d36dd1bb74 --- /dev/null +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adec9978c00bb934eb8bf974c26193e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Shapes/MjMeshFilter.cs b/unity/Runtime/Components/Shapes/MjMeshFilter.cs index 83f4963db7..e509da120c 100644 --- a/unity/Runtime/Components/Shapes/MjMeshFilter.cs +++ b/unity/Runtime/Components/Shapes/MjMeshFilter.cs @@ -41,19 +41,19 @@ protected void Update() { return; } - _shapeChangeStamp = currentChangeStamp; - Tuple meshData = _geom.BuildMesh(); - - if (meshData == null) { - throw new ArgumentException("Unsupported geom shape detected"); - } - if(_geom.ShapeType == MjShapeComponent.ShapeTypes.Mesh) { MjMeshShape meshShape = _geom.Shape as MjMeshShape; _meshFilter.sharedMesh = meshShape.Mesh; return; } + _shapeChangeStamp = currentChangeStamp; + Tuple meshData = _geom.BuildMesh(); + if (meshData == null) + { + throw new ArgumentException("Unsupported geom shape detected"); + } + DisposeCurrentMesh(); var mesh = new Mesh(); From b3274bb91da3c41fd286dc41ba79cfcffe97980f Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 21 Jan 2025 15:21:51 +0000 Subject: [PATCH 3/3] Fix formatting --- unity/Editor/Importer/ObjMeshImportUtility.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs index 2eea221113..262fd12838 100644 --- a/unity/Editor/Importer/ObjMeshImportUtility.cs +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -12,12 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Globalization; using System.IO; using System.Linq; using System.Text; -using UnityEditor; using UnityEngine; namespace Mujoco { @@ -32,7 +30,7 @@ public static class ObjMeshImportUtility { public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, Vector3 scale) { - // OBJ files are human readable + // OBJ files are human-readable string[] lines = File.ReadAllLines(sourceFilePath); StringBuilder outputBuilder = new StringBuilder(); // Culture info for consistent decimal point handling @@ -92,4 +90,4 @@ public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFileP File.WriteAllText(targetFilePath, outputBuilder.ToString()); } } -} \ No newline at end of file +}