Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Unity plugin] Add support for OBJ file import from MuJoCo scenes #2378

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions unity/Editor/Importer/MjImporterWithAssets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mesh>(assetPath);
if (copiedMesh == null) {
throw new Exception($"Mesh {assetPath} was not imported.");
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -289,6 +304,7 @@ private void ResolveOrCreateMaterial(MeshRenderer renderer, XmlElement parentNod
material = DefaultMujocoMaterial;
}
}
if (parentNode.GetFloatAttribute("group") > 2) renderer.enabled = false;
renderer.sharedMaterial = material;
}
}
Expand Down
93 changes: 93 additions & 0 deletions unity/Editor/Importer/ObjMeshImportUtility.cs
Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// 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.
/// </summary>
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());
}
}
}
11 changes: 11 additions & 0 deletions unity/Editor/Importer/ObjMeshImportUtility.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions unity/Runtime/Components/Shapes/MjMeshFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ protected void Update() {
return;
}

_shapeChangeStamp = currentChangeStamp;
Tuple<Vector3[], int[]> 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<Vector3[], int[]> meshData = _geom.BuildMesh();
if (meshData == null)
{
throw new ArgumentException("Unsupported geom shape detected");
}

DisposeCurrentMesh();

var mesh = new Mesh();
Expand Down
2 changes: 1 addition & 1 deletion unity/Runtime/Components/Shapes/MjMeshShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mesh>(assetName);
}
}

Expand Down