From a58f8d0033bfad79aa159ba3f11e6d16f988a21c Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 21 Jan 2025 15:11:27 +0000 Subject: [PATCH] Add OBJ mesh support to Unity plugin --- unity/Editor/Importer/MjImporterWithAssets.cs | 16 ++- unity/Editor/Importer/ObjMeshImportUtility.cs | 131 +++++++++--------- .../Importer/ObjMeshImportUtility.cs.meta | 11 ++ .../Runtime/Components/Shapes/MjMeshFilter.cs | 14 +- 4 files changed, 96 insertions(+), 76 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 9937ce57..4ece4cae 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -174,7 +174,14 @@ public class MjImporterWithAssets : MjcfImporter { 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 @@ public class MjImporterWithAssets : MjcfImporter { // 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 @@ public class MjImporterWithAssets : MjcfImporter { 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 3e2e9885..2eea2211 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); +/// +/// 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); - 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; + 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; - 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); + 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); + + 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]); + } + 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 00000000..d36dd1bb --- /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 83f4963d..e509da12 100644 --- a/unity/Runtime/Components/Shapes/MjMeshFilter.cs +++ b/unity/Runtime/Components/Shapes/MjMeshFilter.cs @@ -41,19 +41,19 @@ public class MjMeshFilter : MonoBehaviour { 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();