diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index a9a93184..9937ce57 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -153,20 +153,21 @@ public class MjImporterWithAssets : MjcfImporter { 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 @@ public class MjImporterWithAssets : MjcfImporter { $"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 @@ public class MjImporterWithAssets : MjcfImporter { 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 00000000..3e2e9885 --- /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 e4fa338d..22721209 100644 --- a/unity/Runtime/Components/Shapes/MjMeshShape.cs +++ b/unity/Runtime/Components/Shapes/MjMeshShape.cs @@ -36,7 +36,7 @@ public class MjMeshShape : IMjShape { var assetName = MjEngineTool.Sanitize( mjcf.GetStringAttribute("mesh", defaultValue: string.Empty)); if (!string.IsNullOrEmpty(assetName)) { - Mesh = (Mesh)Resources.Load(assetName); + Mesh = Resources.Load(assetName); } }