diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index a9a93184d6..4ece4caeb0 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -153,28 +153,36 @@ 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) { throw new Exception( $"Trying to import mesh {unsanitizedAssetReferenceName} but {assetPath} already exists."); } + AssetDatabase.ImportAsset(assetPath); - var copiedMesh = AssetDatabase.LoadMainAssetAtPath(assetPath) as Mesh; + 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."); } @@ -186,9 +194,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) { @@ -275,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(); @@ -289,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 new file mode 100644 index 0000000000..262fd12838 --- /dev/null +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -0,0 +1,93 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Mujoco { + +/// +/// 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) { + // 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; + + 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()); + } +} +} 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(); 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); } }