File: MjImporterWithAssets.cs

package info (click to toggle)
mujoco 2.2.2-3.2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 39,796 kB
  • sloc: ansic: 28,947; cpp: 28,897; cs: 14,241; python: 10,465; xml: 5,104; sh: 93; makefile: 34
file content (287 lines) | stat: -rw-r--r-- 13,310 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// 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.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using UnityEditor;
using UnityEngine;

namespace Mujoco {

// API for importing Mujoco XML files into Unity scenes.
public class MjImporterWithAssets : MjcfImporter {

  private const string _semiTransparentMaterialName = "mujoco_semitransparent_template";

  private string _sourceMeshesDir;
  private string _targetMeshesDir;
  private string _targetAssetDir;
  private unsafe MujocoLib.mjModel_* _mjModel = null;

  // Imports the scene from the specified file, which should be a well-formed MJCF document.
  // The imported scene will be placed under a single node with the requested name assigned.
  //
  // Args:
  //   filePath: Path to XML file.
  //
  // Throws:
  //   Exception if the parsed XML is malformed or contains rough errors. If an exception is thrown,
  //   the entire imported scene will be automatically deleted.
  // TODO(etom) - reconsider unencouraged pattern of validation through exception side-effects.
  public unsafe GameObject ImportFile(string filePath) {
    // If MuJoCo can't parse the mjcfString, we abort the entire process.
    // MjEngineTool.LoadModelFromString throws an exception when MuJoCo fails to parse the provided
    // mjcfString.
    var name = Path.GetFileNameWithoutExtension(filePath) + $"{UnityEngine.Random.Range(0,999)}";
    string newPath = Path.Combine(Application.temporaryCachePath, $"{name}.xml");
    _mjModel = MjEngineTool.LoadModelFromFile(filePath);
    MjEngineTool.SaveModelToFile(newPath, _mjModel);
    Debug.Log($"Imported MJCF loaded, saved to {newPath}");
    string mjcfString = File.ReadAllText(newPath);
    GameObject root = null;
    try {
      root = ImportString(mjcfString, name, filePath);
    } finally {
      MujocoLib.mj_deleteModel(_mjModel);
    }
    return root;
  }

  public GameObject ImportString(
      string mjcfString, string name = null, string filePath = null) {
    var mjcfXml = new XmlDocument();
    mjcfXml.LoadXml(mjcfString);
    // Determine the folder where the meshes are stored.
    ConfigureMeshPath(filePath, name, mjcfXml);
    GameObject sceneRoot = null;
    try {
      sceneRoot = ImportXml(mjcfXml, name);
    } catch (Exception) {
      // We consider any error as critical, and end the import process immediately, cleaning up
      // any assets created.
      AssetDatabase.DeleteAsset(_targetAssetDir);
    }
    return sceneRoot;
  }

  protected override void ParseRoot(GameObject parentObject, XmlElement parentNode) {

    // This makes no references, and should be parsed first.
    var assetNode = parentNode.SelectSingleNode("asset") as XmlElement;
    if (assetNode != null) {
      ParseAssets(assetNode);
    }
    base.ParseRoot(parentObject, parentNode);
  }

  protected override void ParseGeom(GameObject parentObject, XmlElement child) {
    var gameObject = CreateGameObjectWithUniqueName<MjGeom>(parentObject, child);
    gameObject.AddComponent<MjMeshFilter>();
    var renderer = gameObject.AddComponent<MeshRenderer>();
    ResolveOrCreateMaterial(renderer, child);
  }

  private void ConfigureMeshPath(string path, string projectName, XmlDocument mjcf) {
    // Crate the mesh target directory.
    _targetMeshesDir = Path.Combine(
        Application.dataPath, "Local", "MjImports", projectName, "Resources");
    _targetAssetDir = Path.Combine("Assets", "Local", "MjImports", projectName, "Resources");
    if (!Directory.Exists(_targetMeshesDir)) {
      Directory.CreateDirectory(_targetMeshesDir);
    }
    if (string.IsNullOrEmpty(path)) { // we're loading a string
      return;
    }
    _sourceMeshesDir = Path.GetDirectoryName(path);
    var compilerNode = mjcf.SelectSingleNode("/mujoco/compiler") as XmlElement;
    if (compilerNode != null) {
      // Parse the location of meshes
      var relativeMeshDir = compilerNode.GetStringAttribute("meshdir", defaultValue: string.Empty);
      _sourceMeshesDir = Path.Combine(_sourceMeshesDir, relativeMeshDir);
    }
    Debug.Log(
        $"Meshes locations: source = {_sourceMeshesDir}, target = {_targetMeshesDir}");
  }

  private void ParseAssets(XmlElement parentNode) {
    foreach (var child in parentNode.SelectNodes("descendant::mesh").OfType<XmlElement>()) {
      _modifiers.ApplyModifiersToElement(child);
      ParseMesh(child);
    }
    foreach (var child in parentNode.SelectNodes("descendant::material").OfType<XmlElement>()) {
      _modifiers.ApplyModifiersToElement(child);
      ParseMaterial(child);
    }
    AssetDatabase.SaveAssets();
  }

  private void ImportMeshFromModel(int meshIndex) {
  }

  // Using mesh assets involves:
  // (1) Copying the asset to a Resources folder, and rescaling it during that operation.
  // (2) Allowing Unity to parse it using a registered asset importer (STLMeshImporter).
  // (3) Loading that asset as a Mesh resource when the referring geom is parsed.
  //
  // We're also using a dedicated folder to deploy the meshes that are being imported, so that the
  // user can find all meshes loaded by importing a specific model.
  private void ParseMesh(XmlElement parentNode) {
    if (parentNode.HasAttribute("vertex")) {
      throw new NotImplementedException("XML with explicit mesh info not supported yet.");
    }
    var fileName = parentNode.GetStringAttribute("file");
    // If we want to use the element name as the asset name, we must sanitize it:
    var unsanitizedAssetReferenceName =
      parentNode.GetStringAttribute("name", defaultValue: string.Empty);
    var assetReferenceName = MjEngineTool.Sanitize(unsanitizedAssetReferenceName);
    var sourceFilePath = Path.Combine(_sourceMeshesDir, fileName);
    var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName + ".stl");
    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");
    // 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;
    if (copiedMesh == null) {
      throw new Exception($"Mesh {assetPath} was not imported.");
    }
    copiedMesh.RecalculateNormals();
    copiedMesh.RecalculateTangents();
    copiedMesh.RecalculateBounds();
  }

  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);
  }

  private void ParseMaterial(XmlElement parentNode) {
    var rgba = parentNode.GetFloatArrayAttribute(
      "rgba", defaultValue: new float[] {1.0f, 1.0f, 1.0f, 1.0f});
    var emission = parentNode.GetFloatAttribute("emission", defaultValue: 0.0f);
    var reflectance = parentNode.GetFloatAttribute("reflectance", defaultValue: 0.0f);
    var specular = parentNode.GetFloatAttribute("specular", defaultValue: 0.5f);
    var shininess = parentNode.GetFloatAttribute("shininess", defaultValue: 0.5f);
    var unsanitizedName = parentNode.GetStringAttribute("name", defaultValue: string.Empty);
    var name = MjEngineTool.Sanitize(unsanitizedName);
    var albedo = new Color(rgba[0], rgba[1], rgba[2], rgba[3]);

    // Mujoco uses a Blinn/Phong shading model with the addition of reflectance. Unfortunately, at
    // the moment of writing this comment, Unity does not come with a compatible shader. The closest
    // results can be achieved using the Standard shader that implements the Cook-Torrence shading
    // model.
    Material material;
    if (rgba[3] < 1f) {
      material = new Material(AssetDatabase.LoadMainAssetAtPath(
        AssetDatabase.GUIDToAssetPath(
          AssetDatabase.FindAssets(_semiTransparentMaterialName)[0])) as Material);
    } else {
      material = new Material(Shader.Find("Standard"));
    }
    material.SetColor("_Color", albedo);
    material.SetFloat("_Metallic", reflectance);

    // In order to convert the specular/shininess parameters into glossiness/roughness,
    // we perform a coarse approximation.
    // In Blinn/Phong model, Shininess corresponds to the width of the specular spot, while
    // the Specularity corresponds to its strength (how visible it is). In order to approximate it
    // with a single parameter Glossiness, we will assume that the largest value wins. The model
    // will break at the extremes (Spec~=1 & Shin~=0, Spec~=0 & Shin~=1), however it should be
    // representative for the intermediate values.
    float glossiness = Math.Max(specular, shininess);
    // We observe that any reflective material is automatically glossy, by the property of not
    // having rough surface that would scatter the incoming light.
    // Instead of modifying the Shininess parameter however, we're dirrectly modifying
    // the glossiness by bringing the value closer to the upper boundary.
    glossiness = (1.0f - reflectance) * glossiness + reflectance;
    material.SetFloat("_Glossiness", glossiness);

    // We choose to define a simple emission model that only emits light, without scaling
    // the brightness of the defined color. If the user requires, they should tweak the material
    // settings manually.
    if (emission > 0.5f) {
      material.EnableKeyword("_EMISSION");
      material.SetColor("_EmissionColor", albedo);
    }
    var assetPath = Path.Combine(_targetAssetDir, name + ".mat");
    if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) {
      throw new Exception(
        $"Trying to create material {unsanitizedName} but {assetPath} already exists.");
    }
    AssetDatabase.CreateAsset(material, assetPath);
  }

  // Loads a named material asset, or creates an ad-hoc material asset for the specific node.
  private void ResolveOrCreateMaterial(MeshRenderer renderer, XmlElement parentNode) {
    // When the asset was parsed and stored its name was sanitized, so we should load it using a
    // sanitized name:
    var materialName =
      MjEngineTool.Sanitize(parentNode.GetStringAttribute("material", defaultValue: string.Empty));
    Material material = null;
    if (!string.IsNullOrEmpty(materialName)) {
      var assetPath = Path.Combine(_targetAssetDir, materialName + ".mat");
      material = (Material)AssetDatabase.LoadAssetAtPath(assetPath, typeof(Material));
    } else {
      // Nodes may contain inlined color definitions, which override the assigned material colors.
      if (parentNode.HasAttribute("rgba")) {
        // We need a bespoke copy of the material from the database for this particular node.
        var rgba = parentNode.GetFloatArrayAttribute(
          "rgba", defaultValue: new float[] {1.0f, 1.0f, 1.0f, 1.0f});
        if (rgba[3] < 1f) {
          material = new Material(
            AssetDatabase.LoadMainAssetAtPath(
              AssetDatabase.GUIDToAssetPath(
                AssetDatabase.FindAssets(_semiTransparentMaterialName)[0])) as Material);
        } else {
          material = new Material(Shader.Find("Standard"));
        }
        material.color = new Color(rgba[0], rgba[1], rgba[2], rgba[3]);
        // 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");
        if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) {
          throw new Exception(
            $"Creating a material asset for the geom {name}, but {assetPath} already exists.");
        }
        AssetDatabase.CreateAsset(material, assetPath);
        AssetDatabase.SaveAssets();
        material = AssetDatabase.LoadMainAssetAtPath(assetPath) as Material;
      } else {
        material = DefaultMujocoMaterial;
      }
    }
    renderer.sharedMaterial = material;
  }
}
}